I am new to MVC and trying to implement what I would expect to be a common problem. I have a simple search form that I want to implement on each page of the site. I want this section to maintain its own code so that I don't have to duplicate it on each page.
So far I have been able to do this by calling a render action on the template page. The render action populates the quicksearch form. When I submit the form I am able to validate the form, however I have not found a way to redisplay the same page with the validation information. I would prefer a way that would just refresh the form area, but I would accept a full postback as long as the page is redisplayed.
Template Render Call
#{Html.RenderAction("Display", "QuickSearch");}
ActionController
[HttpPost]
public ActionResult Submit(QuickSearchModel qsModel)
{
if (!ModelState.IsValid)
{
return PartialView(qsModel);
}
//Perform redirect
}
[ChildActionOnly]
public ActionResult Display()
{
//populate model
return View(qsModel);
}
Quick Search View
<div>
#using (Html.BeginForm("Submit", "QuickSearch"))
{
#Html.ValidationSummary(true)
#Html.LabelFor(m => m.Destination)#Html.EditorFor(m => m.Destination)#Html.ValidationMessageFor(m => m.Destination)<br />
#Html.LabelFor(m => m.ArrivalDate)#Html.EditorFor(m => m.ArrivalDate)#Html.ValidationMessageFor(m => m.ArrivalDate)
#Html.LabelFor(m => m.DepartureDate)#Html.EditorFor(m => m.DepartureDate)#Html.ValidationMessageFor(m => m.DepartureDate)<br />
#Html.LabelFor(m => m.Adults)#Html.DropDownListFor(model => model.Adults, new SelectList(Model.AdultsSelectOptions, "value", "text", Model.Adults))<br />
#Html.LabelFor(m => m.Children)#Html.DropDownListFor(model => model.Children, new SelectList(Model.ChildrenSelectOptions, "value", "text", Model.Children))<br />
<input id="qsSubmit" name="qsSubmit" type="submit" value="Submit" />
}
</div>
Thanks in advance for any assistance!
I see that you have the following problems:
How to redirect back to the page, from which the search was made?
What if this original page was "POSTed to" - that is it was rendering something, based on POST request? In this case we will not
be able to "re-produce" this POST in any easy way;
After we have redirected to the original page, how to communicate the search model (or just it's errors), which we have failed to
validate?
Given all these challenges, I would first seriously consider making this search form in AJAX style. That would be much easier solution, if it fits you.
If AJAX is not an option I see following solutions to the respective questions:
I would make a hidden field in the search form with the URL of original page. When validation fails, we can redirect to this URL (just check that it is local URL and no one tries to brake something);
This is a major problem - trying to replay original POST is not easy, but it may not be needed either - just check if this is a problem anyway;
You could use TempData dictionary to communicate errors or model back to original page.
Related
I am working on an MVC 5 application and have the following scenario.
The user clicks my registration link to create an account and is served with a view like so. Works as expected.
[HttpGet]
[AllowAnonymous]
public ActionResult RenderRegisterModal(string userType)
{
IList<UserTypesDto> userTypes = _userService.GetUserTypes();
var usersRegisterUserViewModel = new UsersRegisterUserViewModel {UserTypes = new List<SelectListItem>()};
usersRegisterUserViewModel.UserTypes.Add(new SelectListItem
{
Text = "Select a registration type",
Value = "0"
});
foreach (UserTypesDto userTypeDto in userTypes)
{
usersRegisterUserViewModel.UserTypes.Add(new SelectListItem
{
Text = userTypeDto.UserType,
Value = userTypeDto.UserTypeId.ToString()
});
}
return PartialView("_RegisterModal", usersRegisterUserViewModel);
}
Now based on the type of customer / userType they choose I need to present different views. Should not have any issue getting the view into the modal as it is ajax... So lets call this a pseudo wizard to collect data. The issue I am having and the basis for this probably simple question, that I am thinking too much about, is how do I save the data from each step?? Temp Table? InMemory Cache using the session id as the key? Cookies? "Gross"
The ActionMethod that the post goes to looks like this.
[AllowAnonymous]
[HttpPost]
public ActionResult Register(UsersRegisterUserViewModel usersRegisterUserViewModel)
{
//TODO Return a view for the next step based on the CustomerType contained in the viewModel
return View();
}
Any feedback would be greatly appreciated.
OPTION 1:
The easiest way, is that the NEXT view have a set of hidden fields with the information entered in the previous view and which was posted to the Action.
This way, once you post the second view, you will be posting all the information (previous one and the one you entered in the second view).
OPTION 2:
If you are not comfortable with the first approach, you can have several PartialViews that are shown or hidden in javascript on UserType combo changes. Your ViewModel should have all the propertied you need to hold the information before posting back to server.
This option came in 2 flavors: you render all your usertype partials at the begining (so you need to hide them at first), or you can get the partial via Ajax once the user selected one user type.
OPTION 3:
You can use Session to hold the sensitive data from the register form, redirect to the next view depending on user type, post the new form, and in the Action you retrieve the information from Session... and with all the information in your hands store it in database.
If you're using razor, persist the different parts of your model like this
#Html.EditorFor(model => model.data1)
#Html.EditorFor(model => model.data2) #*Wizard part 1*#
then in the next view for your wizard
#Html.HiddenFor(model => model.data1)
#Html.HiddenFor(model => model.data2)
#Html.EditorFor(model => model.data3)
#Html.EditorFor(model => model.data4) #*Wizard part 2*#
and then
#Html.HiddenFor(model => model.data1)
#Html.HiddenFor(model => model.data2)
#Html.HiddenFor(model => model.data3)
#Html.HiddenFor(model => model.data4)
#Html.EditorFor(model => model.data5) #*Wizard part 3...*#
and so on...
With the help of several SO questions, I've figured out how to use two models on the same view, using the tuple form. At the top of my file is this:
#using Project.Models;
#{
ViewBag.Title = "Details";
Layout = "~/Views/Shared/_Layout.cshtml";
#model Tuple<Foo, Bar>
}
For the Foo stuff, it uses jQuery like this:
#Html.DisplayFor(tuple => tuple.Item1.ID)
and works fine. However, for my second model, it isn't displaying info, but is a submission form. Currently, this is what I have:
#using (Html.BeginForm(null, null, FormMethod.Post, new { id = "createFoo", #action = "/api/Foo" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="editor-field">
#Html.TextAreaFor(tuple => tuple.Item2.Text)
#Html.ValidationMessageFor(tuple => tuple.Item2.Text)<br />
</div>
<input type="submit" value="Post Response" />
}
Mind you, this is mostly copy paste from other views since I'm new to MVC and it worked fine with other forms. For my FooController, I have this:
public void Post([FromBody] Foo foo)
{
Foo existingFoo = this.fooRepository.GetFoo(foo.ID);
if (existingFoo != null)
{
// throw error
}
else
{
System.Diagnostics.Debug.WriteLine("MESSAGE POSTING: " + foo.Text);
}
}
When submitting from the view, the received foo isn't null (checked in other tests), but the foo.text is empty. I've tried lots of different inputs and such, but I'm just so unfamiliar with the #Html.* functions and ASP.net-MVC in general that I'm not sure where I could be going wrong.
If you need to see my repository code let me know, but I doubt it'd have an effect on this. Thanks in advance for any help.
There are 2 issues I can see with your code above:
The way the Html helper will output your fields and how that feeds into your api post
Not having your ID in the controller.
1: Outputting a field like this (#Html.TextAreaFor(tuple => tuple.Item2.Text)) will give the field the name "Item2.Text". This is an issue as that is what gets passed on the post. You will get form data with Item2.Text:dfsdsgdfg. This won't match when your API controller tries to bind. So, try outputting the text field with a set name:
#Html.TextArea("Text", #Model.Item2.Text)
2: Your Id field is not in the form... thus it won't be sent. Try using a hidden field:
#Html.Hidden("ID", #Model.Item1.ID)
Also, just a clarification, this (#Html.DisplayFor(tuple => tuple.Item1.ID)) is not jQuery.
Im kinda new in MVC4 and im not able to figure it out.
"CustomViewMOdel" "CustomViewMOdel"
"ControllerX" ----------------> "VIEW" -----------------> "ControllerY"
My problem is that i want to pass my customviewmodel to view (which is working just fine!). In the View im showing some of model's fields to users (which is working fine also). BUT Now i want user, to change ONE field of the models fields and then PASS the WHOLE model to Controller X (with all fields filled, including the field what user was able to change AND other fields what were just shown)
Can anyone give a very simple code example of how to do this?
You can just create a form that posts to another controller:
ControllerX:
public ActionResult DoSomething()
{
return View(new CustomVM());
}
ViewA
#Model CustomViewModel
#using Html.BeginForm("DoSomethingElse", "ControllerY")
{
#Html.EditorFor(vm => vm.SomeProperty)
<input type="submit" value="OK" />
}
ControllerY
public ActionResult DoSomethingElse(CustomViewModel vm)
{
// do something else
}
You can use #Html.HiddenFor(o => o.Property) on the form.
This will not show a property on it.
But the advanced user may change the property through a development console. So you should check all the changes in the ControllerY
Example:
#Html.HiddenFor(o => o.Id)
#Html.HiddenFor(o => o.Name)
#Html.EditorFor(o => o.Description)
<input type="submit" value="OK" />
This will only let the user change a description but still have "id" and "name" on the FormCollection.
My one view page I passed in a model through the controller, so I can write:
#Html.DisplayFor(m => m.FirstName) which displays the First Name of the model. When I try to submit the form on the page using
#using (Html.BeginForm("CreateUser", "Controller", FormMethod.Post, new { UserViewModel = Model }))
and I take a look at my model in
[HttpPost]
public virtual ActionResult CreateUser(UserViewModel model)
{
//model.FirstName is blank
Is there anyway I can make the model.FirstName not blank? by some how passing the model that I originally passed? I could set a bunch of hidden remarks, though if there is a better way that would be very helpful
EDIT: the DisplayFor is just an example to show the model is accessible. I actually have about 15 fields, and I am going through multiple forms trying to populate the model. Is Hidden the only way? and could I just hide the entire model?
#Html.DisplayFor() creates a simple literal with the value by default (if you're not using a display template), on submit only form elements being submitted to the server.
You can use hidden input.
#Html.HiddenFor(m => m.FirstName)
Which will be something like:
<input type="hidden" value="{the first name}" name="FirstName" id="FirstName" />
After so many years using ASP.Net, I’m still trying to figure out how to achieve the same results using MVC.
I have a materpage with a control that is strongly type to something. When I navigate to a view of a different strongly type model ...and click on the button to execute something, I get "The model item passed into the dictionary is of type Site.Models.RegisterModel', but this dictionary requires a model item of type Site.Models.LogOnModel'".
For the sake of this example, we can take the Default MVC app that is provided with VS 2010, let’s imagine I want to change the “LogonUserControl.ascx” so that it either tells me the logged user (as it works currently) OR allow me to login from there, showing me the text boxes for username and password (therefore in this case from the home page).
So I take the control and strongly type it as:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Gioby.Models.LogOnModel>" %>
<%
if (Request.IsAuthenticated) {
%>
Welcome <b><%: Page.User.Identity.Name %></b>
[ <%: Html.ActionLink("Log Off", "LogOff", "Account")%> ]
<%
}
else {
%>
<% using (Html.BeginForm()) { %>
<div id="logon">
<div class="editor-label">
<%: Html.LabelFor(m => m.UserName)%>
<%: Html.TextBoxFor(m => m.UserName)%>
<%: Html.ValidationMessageFor(m => m.UserName, "*") %>
<%: Html.LabelFor(m => m.Password)%>
<%: Html.PasswordFor(m => m.Password)%>
<%: Html.ValidationMessageFor(m => m.Password, "*") %>
<input type="submit" value="Log On" />
</div>
<div class="editor-label">
<%: Html.ActionLink("Register here", "Register", "Account")%>
<%: Html.CheckBoxFor(m => m.RememberMe, new { #class = "pad-left" })%>
<%: Html.LabelFor(m => m.RememberMe) %>
</div>
</div>
<% } %>
<%
}
%>
Then on the HomeController, I add a procedure as:
[HttpPost]
public ActionResult Index(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
// ==>> Check Login against your DB
// Now check if param returnUrl is empty
if (!String.IsNullOrEmpty(returnUrl))
return Redirect(returnUrl);
return RedirectToAction("Index", "Home");
}
// If we got this far, something failed, redisplay form
return View(model);
}
I tested it from the home page … it works !!!
BUT when I navigate to the “Register” view (remember that the “LogonUserControl.ascx” is located inside the “MasterPage”, therefore visible from the Register view).
So when I click on the Register button, I get the error:
The model item passed into the dictionary is of type Site.Models.RegisterModel', but this dictionary requires a model item of type Site.Models.LogOnModel'.
QUESTION:
Does that mean that I will never be able to different pieces together into one view?
Let’s say I want to write an eCommerce site and on the home page I want to see “Most used Tags”, “Most bought products”, “Product of the Month”, “List of Categories” …all within the same view and each one with his own HTTP POST action.
If this is possible using MVC?
If I'm understanding the problem correctly, you have two Views that use the same MasterPage, but which are strongly typed against different ViewModels. The master page is able to include a Partial View that is also strongly typed, as long as its expected ViewModel is the same as that of the parent view. However, if you're using a view with a different ViewModel type, it doesn't know what to do.
Consider the following:
<% Html.RenderPartial("LogOn") %>
The above code implicitly includes the model data for the current view being rendered. It's exactly the same as if you had said:
<% Html.RenderPartial("LogOn", Model) %>
So this will only work if Model is a LogOnModel. Remember that the MasterPage is really a part of whatever View inherits it, so even if you're putting this in the MasterPage, it's as if you'd put the same code in every view that inherits it. So if your View's Model is not the same as the PartialView's Model, this won't work. One alternative is to use inheritance to ensure that every ViewModel will include all the information required by the Master Page. This approach is described in detail here.
But that approach means that you have to always use a factory to produce your view model, and every view model has to be somewhat aware of which master page it will use. In our product, we can use a different master page on the same view depending on what mode the user is viewing the site in, so it doesn't make sense to tie the ViewModel to that of the Master Page. We accomplish what you're describing using the RenderAction method, which allows you to render an entire controller action as if it were just a part of the larger view. Some of the advantages of this approach are discussed here.
So now you can have your MasterPage include whatever little partial views you want, but you separate the logic for building the ViewModel of each of these Views into an individual controller action that's responsible for that particular Partial View:
<% Html.RenderAction("LogOnBox") %>
The Action:
public ActionResult LogOnBox()
{
LogOnModel model = GetLogOnModel();
return PartialView("LogOnUserControl", model);
}
Now, regardless of what model your current view uses, your Master Page can include “Most used Tags”, “Most bought products”, “Product of the Month”, “List of Categories”, etc. Better still, these portions of the page can leverage output caching so they don't have to be regenerated with every page load if they don't change very often.