There are lots of questions asking about this logic in reverse, however, I can't find an answer for my issue. I'm new to MVC so might not have the terminology specified correctly.
I have a View containing a form, where a user can request a product. The same View is used regardless of the product, but specific fields are shown in the form relating to that particular product, e.g.
public class RequestController : Controller
{
// This bit works fine and displays the appropriate form in the view
public ActionResult MyProduct(string id)
{
if (string.IsNullOrWhiteSpace(id))
{
return new HttpNotFoundResult();
}
ProductRequest qd = new ProductRequest();
switch (id)
{
case "Beer":
qd.RequestType = Models.RequestType.Beer;
break;
case "Coffee":
qd.RequestType = Models.RequestType.Coffee;
break;
case "Soda":
qd.RequestType = Models.RequestType.Soda;
break;
}
return View("Index", qd);
}
// Need to get all forms rendered by the above to post here...
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult MyProduct(ProductRequest qd)
{
if (!ModelState.IsValid)
{
return View(qd);
}
else
{
// To do ...
}
}
}
When the form is rendered in the view, using...
#using (Html.BeginForm())
...the rendered HTML shows a different form target URL for each product type, e.g:
<form action="/Request/MyProduct/Beer" method="post">
Can I make the form action property use the same controller/method regardless of the product type? e.g.
<form action="/Request/MyProduct" method="post">
I assume there are different ways to achieve this given the flexibility MVC seems to offer, but I'm looking for best practice please as a learning experience.
The Html.BeginForm() call accepts various parameters, which can declare the controller/method you wish the form to use.
#using (Html.BeginForm("MyProduct", "Request", FormMethod.Post, new { enctype = "multipart/form-data"}))
Should produce:
<form action="/Request/MyProduct" enctype="multipart/form-data" method="post">
What added value gives you the Html.BeginForm helper over writting the html yourself, apart from preventing you to easily control your action url?
Personally, I have banned most Html helpers usages, there are just getting in my way instead of helping me.
Write your html code yourself, that is frequently even more concise than using html helpers (as illustrated in this answer).
<form action"#Url.Action("MyProduct", "Request")" method="post">
#* Add [ValidateAntiForgeryToken, HttpPost] attributes to target action *#
#Html.AntiForgeryToken()
...
</form>
(And even better, use UrlHeper extensions to define and centralize your URIs, instead of calling Url.Action inside your views or controllers.)
For completeness, I should mention that most input fields html helper extensions (those suffixed with For) have the added value of exploiting data-annotations of your view-models. But I consider those annotations as bad practice, because they handle subjects which are not the view-model responsibility in strict MVC pattern. (Display labels? Should be controlled by the view. Validation rules? Should be controlled by the controller. Action argument and/or parameter model? No reason to be bound to the view-model names.)
Related
From what I read in various tutorials, or simply the sample project, the Controller use the ViewBag to store anything that will be dipsplayed on the page.
In the Controller I can just type ViewBag.AnythingIWant = "Foo";, and it'll render in the HTML. Apparently that's what is done in the sample project, at least for title and various texts.
But most of the text is hardcoded in the HTML and obviously I don't want that. Considering I'm not new to C# or MVC in Xamarin (mobile development), I feel like I should grasp this pretty quick, but I don't. Could someone clarify to me the following :
My Controller knows the ViewModel (which does most of the work) and himself uses the Model privately. I'm used (from iOS dev) for the controller to be the last layer of UI, and inside the controller I would just have all my labels and whatever I want, and I can fill them with whatever is available in the ViewModel.
Here, there is this HTML layer that I don't know how to connect to the controller.
I have a strong feeling that putting everything in the ViewBag cannot be the way to go. Could anyone briefly reveal the piece I am missing to use proper objects inside the HTML ?
Razor might be what's confusing me, considering whatever I add publicly in my Controller, I can't find it in the related HTML using #MyProperty
I know this is pretty broad question but I know that I only miss a small piece of knowledge to unlock everything.
As noted in the comments, pass a ViewModel to the View to be rendered!
Controller
public ActionResult Index() {
var viewModel = new MyViewModel { Name = "some string" };
return View("Index", viewModel);
}
Index.cshtml
#model MyViewModel #* Tell Razor which ViewModel to expect *#
#{ string name = Model.Name; }
I have a problem with a mvc4 project.
I try sending some text from a view to a controller which then add it in a model and then I try to retrieve the text in another view.
The first views code looks like this:
#{
Html.BeginForm("Result", "Search", "test");
}
<div>
</div>
The controller code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using TheLibrary.Models;
namespace TheLibrary.Controllers
{
public class SearchController : Controller
{
public ActionResult Result(string text)
{
var searchCriteria = new Search { searchCriteria = text };
ViewBag.Search = searchCriteria;
ViewData["ViewSearch"] = searchCriteria;
TempData["TempSearch"] = searchCriteria;
return View();
}
}
}
My models code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace TheLibrary.Models
{
public class Search
{
public string searchCriteria { get; set; }
}
}
The second views code:
#Model TheLibrary.Models.Search
#{
var text = (ViewBag.Search as TheLibrary.Models.Search).searchCriteria;
}
<div>
#text
</div>
The result I get is "TheLibrary.Models.Search" rather than the expected "test".
What am I doing wrong?
With all due respect, you really need to work through some MVC tutorials. I'm not usually a fan of Microsoft documentation (especially their insipid 'quickstarts') but their MVC tutorials are actually quite good. That said, let me clear some things up for you.
Firstly, the whole point of creating a view model (Search in this case) is so that you don't have to use ViewBag or ViewData, to give you a guaranteed way of accessing your model's data. ViewBag and ViewData are virtually the same thing. The main point here though is the idea to move away from these in favour of using strongly-typed views.
So, that begs the question, what is a strongly-typed view? Put simply, a strongly-typed view is a view that requires a specific type to be passed to it. You specify a strongly-typed view by using the #model directive at the very top of the view. In your case, this would be:
#model TheLibrary.Models.Search
Note: The lowercase m. Do not confuse this with #Model which is a way of accessing your strongly-typed model.
Being as we're telling our view exactly what type we're going to pass to it, we can use #Model to access the properties of the model, like so:
<div>
#Model.searchCriteria
</div>
(Please also note that guidelines suggest using Pascal Case for properties.)
Now, let's clear up the issue with your controller. Firstly, as we're already using our model, as discussed above, there is no point in using ViewBag or ViewData. TempData serves an altogether different purpose and has no bearing on your question. (See this answer for when you might want to use it.) In that sense, it is also not needed. Lastly, you would simply pass the model to the view itself:
public ActionResult Result(string text)
{
var model = new Search { searchCriteria = text };
return View(model);
}
Now, your complete view would look like this:
#model TheLibrary.Models.Search
<div>
#Model.searchCriteria
</div>
However, as stated at the start of this answer, I really believe you should look into a few tutorials because there are things here that you're still not aware of (such as passing searchCriteria directly to your view without any kind of validation).
Update per comments
Your view wants to look something like this:
#using (Html.BeginForm("Result", "Search", FormMethod.Get))
{
#Html.TextBox("text")
<input type="submit" value='Search' />
}
There are a couple of things to note.
Firstly, the FormMethod.Get is ensuring this form's data is sent via GET instead of POST. This is what puts text into the query string in your result's URL. So it makes it look something like:
http://somewebsite.com/Search/Result?text=test
This is good because it means if the user refreshes the page, they're not asked to resubmit the form and it also acts in the same way every time the page is accessed from that URL.
Secondly, #Html.TextBox("text"). The string you supply to the TextBox() method is the name that is given to that textbox, and it is also the name that is used to lookup the data in that textbox. That means it has to match the name of the parameter in your Result action (or part of a model, but let's keep things simple for now), so this part is important for it to work correctly.
So the idea here is that for you to get the correct data for your searchCriteria in your Result view, using your example, is that the user would type test into the textbox. They'd then click the Search button which would get MVC to call the Result action. When doing so, it would use what's called 'model binding' to match the value of the text textbox to the text parameter in your action. That means when we get here:
public ActionResult Result(string text)
{
// ...
}
text will have the value of whatever someone has typed into the textbox. You then pass that to your model which is then passed to the view.
#Model should not be capitalized. Then you can just write:
#model TheLibrary.Models.Search
<div>
#Model.SearchCriteria
</div>
To be clear, the first #model should not be capitalized when you are declaring the type. After that you capitalize it to reference it.
Also I am not sure why you are using the ViewBag and not just passing in the model to the view.
return View(search);
Also I don't recommend this naming scheme:
var searchCriteria = new Search{ searchCriteria = text };
becuase then you have write searchCriteria.searchCriteria. Instead call it what it is:
var search = new Search...
(This post is candy-obfuscated; obfuscated because I have to, candy for the lulz. Trust me, the real thing is actually worthwhile.)
Using ASP.NET MVC 4 and EF 5, I'm trying to create a page that simultaneously shows a list of entities that currently exist in the database and a simple create field at the bottom. I have a functioning way of doing it, but I'm wondering if there's a better way, because my current one feels very roundabout. I'd upload an image of what I've got, but I have to have at least ten reputation, so... on to the post.
The model I'm passing in looks like so:
public class CandyBrand
{
public int ID { get; set; }
[Required(ErrorMessage = "Brand name is required.")]
[Display(Name = "Brand Name")]
public string Name { get; set; }
}
The controller looks like this, including both GET and POST methods:
public ActionResult CandyBrands()
{
return View(context.CandyBrands);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CandyBrands(CandyBrand brand)
{
if (ModelState.IsValid)
{
context.CandyBrands.Add(brand);
context.SaveChanges(); //try/catch block removed for brevity
}
return View(db.CandyBrands);
}
And my view:
#model IEnumerable<CandyDatabase.Models.CandyBrands>
#{
ViewBag.Title = "Brands";
}
<h2>Brands</h2>
<p>#Html.DisplayNameFor(m => m.Name)</p>
#foreach (var brand in Model)
{
<p>#Html.DisplayFor(m => brand.Name)</p>
}
<h3>Create New</h3>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<p>#Html.EditorFor(m => m.FirstOrDefault().Name) <input type="submit" value="Create" /></p>
<p>#Html.ValidationMessageFor(m => m.FirstOrDefault().Name)</p>
}
<p>#Html.ActionLink("Back to Candy List", "Index", "Home")</p>
#section Scripts{
#Scripts.Render("~/bundles/jqueryval")
#Scripts.Render("/Scripts/clear-inputs.js")
}
Because I'm passing in a list of candy brands, the model takes on the type IEnumerable. This isn't a problem for the first half - the foreach loop takes care of this. However, this creates other problems further down the page. Because the model is an IEnumerable, Html.EditorFor doesn't like it. Currently, to solve this, I'm calling FirstOrDefault, which brings it down to one entry. This is annoying in and of itself, but it doesn't stop there; MVC then automatically (and unwantedly (which may or may not be a word)) fills the editor with the data from the first entity in the model! If you notice at the bottom, there's a call to a 'clear-inputs' script; this script exists SOLELY to fix this problem by running
$("input[type!='submit']").val("");
to clear all the fields when the page loads.
Is there a better way to do this? I'm not above accepting the answer "Use a view model, dangit!", but it seems silly to have a view model that has a CandyBrand entity and then a list of CandyBrand entities.
So, it appears, according to Henk Holterman
A ViewModel is the actual, simple and clear solution here. Nothing silly about it.
and Darin Dimitrov
Dude, use a view model, dangit! How else are you even thinking in being able to write an ASP.NET MVC application that actually works? Without a view model? I have never seen such a beast released in the wild yet. Coz that ain't gonna happen. The very first thing you should start thinking before even writing a single line of code from your application is how your view models are gonna look like.
that the answer is a view model. Thank you both for your help; henceforth, I will use view models, dangit!
The behavior of the HtmlHelper.Hidden eludes me.
This is from a template inside Views/Shared/DisplayTemplates/Test.cshtml. Idea was to output a delete button next to the element in the list. So the view accepts a model of IEnumerable and then uses #Html.EditorForModel() to output each test item.
So if i put this into Test.cshtml:
<input type="hidden" name="Name" value="#Model.Name"/>
#Html.Hidden("Name2", Model.Name)
That yields this:
<input type="hidden" name="Name" value="test"/>
<input id="RoleList_12__Name2" name="RoleList[12].Name2" type="hidden" value="test" />
Why does the name of the Html helper render something different from what i tell it to? I assume that this is intended, but i don't understand why.
Update
How would i go ahead and retrieve RoleList[12].Name in my controller?
My delete function accepts:
[HttpPost]
public ActionResult DeleteRole(Roles.Test model)
{
}
How can that one accept a RoleList[12] name item? It always returns null if i try.
This is happening because your view accepts IEnumerable of some model type. You basically have a list of objects passed to the view and since they all have a property named Name the binder is trying to distinguish between different instances by giving them names like model[index]property.
If you're trying to delete an instance you should have an Html.ActionLink link that invokes some action of your controller which takes the unique ID of the instance you're trying to delete. Something like this in your view:
#foreach (var item in model)
{
#Html.ActionLink("linkText", "DeleteRole", "controllerName", new {id = item.RoleId})
}
and then your controller should have an action like this:
public ActionResult DeleteRole(int id)
{
// Logic to delete role based on provided role ID.
return View();
}
It's so that the (default) model binding will work; i.e. when posted the field will be resolved to RoleList[12].Name in the Controller that takes a parameter equivalent to the model class.
If your controller action doesn't reference the model class the data will silently be discarded, so that you don't have to post back entire data - but that which is posted back will be realized for you.
You can override the default model binding; see The Features and Foibles of ASP.NET MVC Model Binding
The problem with your delete is that it's at the wrong level; either you bind something to the instance of the enumerable - wrapped up in its own form (ok for small amounts of data) or you decide to have an #ActionLink into which the ID of the enumerable is passed. I generally do the second, something like:
#Html.ActionLink("Del", "DeleteRole", "Controller", new { Id = item.Id}, new { #class = "btn"})
Of course the above doesn't need a [Post] action so that'd need changing on your controller.
The above would sit in your display or edit template as appropriate.
I currently use an ajax post back to an action to create a new database object and then return the rendered html for a view of that object. I then inject that html into the page.
I was wondering, since MVC does easily allow you to render views to HTML from within controllers, if there was a better (or more correct) approach to the problem.
(I currently use this code to render the view as html in the action).
Any and all ideas appreciated.
As a matter of fact there is. Just return a partial view.
public ActionResult AjaxStuff()
{
// do whatever
var model = ...;
return PartialView(model);
}
It's true you can render it using PartialView or doing it custom via JSON (PartialView is just so much easier!).
It really depends on your implementation and choices regarding graceful degradation.
What I usually do is:
[HttpGet]
public ActionResult SignIn()
{
//Check if it is an AJAX request
if (Request.IsAjaxRequest())
return PartialView();
else
return View();
}
So it is possible to get your cake and eat it too.
Truth be told there are various ways to get this view from the server. You could use the asp.net mvc ajax js library, use .getJSON or .ajax, or even do it via JQuery using .load: http://api.jquery.com/load/
You can render a PartialView as the other answer suggested or return JsonResult and populate your HTML from the JSON data. Your call.