MVC Pass display data between views - c#

I have a view model that is used to display a form on one view, and then is also used to represent the POST data to an action. The action then displays another view model that contains much of the same data from the first view model. However, the first view model has several "display only" properties that are also required on the second view model (for display only on the second view also).
I am wondering what the best way to pass this "display only" data to the second view would be. Currently, the best solution I have come up with is to have a bunch of hidden form fields that contain the display only property values, and then the model gets auto-populated for the action that handles the form POST. However, using hidden form fields seems very "hackish", and there seems like there should be a better solution to passing this data to another view The action doesn't need the display only information, it is only accessing it to populate the properties of the second view model that is passed to the second view.
Let me just explain my question with code, as what I am after is probably better understood through code than words.
Models:
public class SearchFilters
{
// ...
}
public class SearchResult
{
public int Id { get; set; }
public bool Selected { get; set; }
public string SomeDisplayValue1 { get; set; }
public string SomeDisplayValue2 { get; set; }
// ...
}
public class ResultsViewModel
{
public IList<SearchResult> Results { get; set; }
// ...
}
public class DoSomethingWithSelectedResultsViewModel
{
public IList<SearchResult> SelectedResults { get; set; }
public string SomeOtherProperty { get; set; }
// ...
}
Controller:
[HttpPost]
public ActionResult Results(SearchFilters filters)
{
ResultsViewModel results = new ResultsViewModel();
// ...
return new View(results);
}
[HttpPost]
public ActionResult DoSomethingWithSelectedResults(ResultsViewModel model)
{
// ...
return View(new DoSomethingWithSelectedResultsViewModel
{
SelectedResults = model.Results.Where(r => r.Selected).ToList(),
SomeOtherProperty = "...",
// ...
});
}
View: Results.cshtml
#model ResultsViewModel
#using (Html.BeginForm("DoSomethingWithSelectedResults", "Search"))
{
<table>
for (int i = 0; i < Model.Results.Count; i++)
{
<tr>
<td>
#Html.CheckBoxFor(m => Model.Results[i].Selected)
#* I would like to eliminate these hidden inputs *#
#Html.HiddenFor(m => Model.Results[i].Id)
#Html.HiddenFor(m => Model.Results[i].SomeDisplayValue1)
#Html.HiddenFor(m => Model.Results[i].SomeDisplayValue2)
</td>
<td>#Html.DisplayFor(m => Model.Results[i].SomeDisplayValue1)</td>
<td>#Html.DisplayFor(m => Model.Results[i].SomeDisplayValue2)</td>
<tr>
}
</table>
<button type="submit">Do Something With Selected Results</button>
}

As far as I know, one of the best way to pass data from View to another View through a Controller is to use ViewBag, ViewData or TempData. As an example, you can pass the data retrieved from View I as shown below:
TempData[DataToBePassed] = model.CustomData;
And then retrieve this data in View II similar to that:
#if(TempData[DataToBePassed] != null)
{
var dataFromFirstView = TempData[DataToBePassed];
}
For more information take a look at When to use ViewBag, ViewData, or TempData in ASP.NET MVC 3 applications.

You could put the model in the TempData property of the controller, that way it's automatically available in the next request.
More here

Found what I was looking for, I just hadn't worked with MVC enough yet to know about it. The Controller.UpdateModel method does exactly what I was looking for.
Example (using the code from the question):
[HttpPost]
public ActionResult DoSomethingWithSelectedResults()
{
// Load initial model data here, in this case I had simply cached the results in
// temp data in the previous action as suggested by Emeka Awagu.
ResultsViewModel model = (ResultsViewModel)TempData["results"];
// Call UpdateModel and let it do it's magic.
UpdateModel(model);
// ...
return View(new DoSomethingWithSelectedResultsViewModel
{
SelectedResults = model.Results.Where(r => r.Selected).ToList(),
SomeOtherProperty = "...",
// ...
});
}
Using this method I was able to eliminate all the hidden form fields and did not have to write any custom copy logic, since UpdateModel deals with it automatically.
Note: I did have to implement some custom model binders to get things to work correctly with dictionaries and collections (see here, here, and here).

Related

How to create VIew of ViewModel, where ViewModel is an IEnumerable with two strings?

I have been working on an MVC ViewModel that contains scalar data values I need to pass to the View (instead of using ViewBags) along with an IEnumerable that contains the tabular data that goes in the table. This was based on a suggestion someone made on an earlier SO question. This way, there is no duplication I would have if I added the scalar values as constants to a table that could have many, many rows.
I am pasting my (simplified) data model and view model definitions and the controller action in question. The data structures are what I need it to be, but Visual Studio won't create a View from this Action (right-click and select "Add View with Model").
My question, is given the Action and Model definitions, what do I need to do so that I can create a View of the View Model?
I'm figuring auto-scaffold is out of the question because the ViewModel has no key, so I'm expecting manual coding. I have no idea how to do this, because all the examples I've seen show ViewModels that are essentially the equivalent of merging two tables into a larger table.
I have tried using #model = [ViewModelName] and #model = IEnumerable, but both failed.
The binary response would be, "Duh, the error is telling you exactly what's going on. You don't have a Key." But that response doesn't help me resolve this. How do I move forward with this definition of the ViewModel and create a View?
View Model
public class StudentRosterViewModel
{
// This list is the "table" shown on the view
public IEnumerable<StudentRoster> StudentRosters { get; set; }
// These are scalar values apply to the view as a whole
public string SelectedCampus { get; set; }
public string SelectedFiscalYear { get; set; }
}
Data Model
[Table("StudentRoster")]
public partial class StudentRoster
{
public int ID { get; set; }
[Required]
[StringLength(3)]
public string Campus { get; set; }
[Required]
[StringLength(4)]
public string FiscalYear { get; set; }
[Required]
[StringLength(50)]
public string StudentName { get; set; }
public int StudentID { get; set; }
}
Controller Action:
public ActionResult FilterableIndex(string campus="MRA",string fiscalYear = "FY16")
{
StudentRosterViewModel vm = new StudentRosterViewModel();
// this is tabular data and goes in the "table"
vm.StudentRosters = db.StudentRosters
.Where(m => m.Campus == campus)
.Where(m => m.FiscalYear == fiscalYear)
.ToList();
// These are scalar values; they are not tabular
// These are needed to supply values to jQuery
vm.SelectedCampus = campus;
vm.SelectedFiscalYear = fiscalYear;
return View(vm);
}
The convention is that in your Views folder there's a folder corresponding to the name of your controller. For example, if the controller is StudentRosterController then the folder would be named StudentRoster.
Then the view would correspond to the name of the action - FilterableIndex.cshtml.
The .cshtml file would begin with
#model MvcApp.Controllers.StudentRosterViewModel
(Where MvcApp.Controllers is the namespace that contains your model.)
Thanks to the help of everyone in the comments (#StephenMuecke, #LinhTuan) and #ScottHannen, I am able to create a working and improved View page of the ViewModel.
Here are the structural improvements I made.
I created a ViewModels folder and placed the ViewModel class there (no longer in the Models folder).
I took out the [Key] attribute and Id property from the ViewModel -- it really wasn't what made it appear to work before.
I right-clicked the Action Name and selected "Create View" (empty without model)
The View was successfully created. The View code you see below is a basic test for ability access the ViewModel properties and display data -- I can add Bootstrap styles and the functionality, now that I have a basic test.
The IEnumerable took a little work to unpack. I later learned how to add a variable var headerMeta = Model.StudentRosters.FirstOrDefault(); so that I could use #Html.DisplayNameFor in the table body section, I picked up another trick (modelItem => item.[column name]).
A potential further step is to create a custom DisplayFor and DisplayNameFor helper so that I can more directly use the Html helpers without having to add the headerMeta variable.
I call this an initial success because I finally have data reaching the view.
#model ViewModelTest.Controllers.ViewModels.StudentRosterViewModel
#{
ViewBag.Title = "FilterableIndex";
#*This enables us to read the StudentRoster column names*#
var headerMeta = Model.StudentRosters.FirstOrDefault();
}
<h2>FilterableIndex</h2>
<p>Test for reception of data</p>
<ul>
<li>Selected campus: #Model.SelectedCampus</li>
<li>Selected fiscal year: #Model.SelectedFiscalYear</li>
</ul>
#*I'll add bootstrap styles later*#
<table>
<tr>
<th>#Html.DisplayNameFor(m => headerMeta.Campus)</th>
<th>#Html.DisplayNameFor(m => headerMeta.FiscalYear)</th>
<th>#Html.DisplayNameFor(m => headerMeta.StudentName)</th>
<th>#Html.DisplayNameFor(m => headerMeta.StudentID)</th>
</tr>
#foreach (var item in Model.StudentRosters)
{
<tr>
<td>#Html.DisplayFor(modelItem => item.Campus)</td>
<td>#Html.DisplayFor(modelItem => item.FiscalYear)</td>
<td>#Html.DisplayFor(modelItem => item.StudentName)</td>
<td>#Html.DisplayFor(modelItem => item.StudentID)</td>
</tr>
}

Model dropping List<>s after DropDownList

I'm working on a search criteria building page. In addition to several string and numerical type fields, there are several "multiple choice" options.
I'm using the [Get] signature without parameters(pass the CriteriaModel to the view) >> [Post] signature with CriteriaModel parameter (redirect to searching controller)
I've built lightweight option classes (just value, name pairs) and am populating several List<> with the primitive options.
Using Html.DropDownListFor, I'm able to get them to display.
...but...
When I enter the [Post] version, the List<>s are all set to null and empty. Further, the other criteria fields supposed to be populated afterwards are also default and empty.
Technically, I don't need a whole list of values back - if I could even just have the index of the selected value - but I'm up against a wall here.
Pertinent model data:
public class CriteriaModel
{
[DisplayName("Owner Name")]
public string OwnerName { get; set; }
[DisplayName("Subdivision")]
public List<Subdivision> Subdivision { get; set; }
[DisplayName("PIN")]
public string PIN { get; set; }
}
public class Subdivision
{
public int ID { get; set; }
public string Name { get; set; }
}
Pertinent controller code:
[HttpGet]
public ActionResult Index()
{
CriteriaModel criteria = new CriteriaModel();
...fill in the Subdivisions...
View(criteria);
}
[HttpPost]
public ActionResult Index(CriteriaModel search_criteria)
{
return View("Search obtained" + search_criteria.Subdivision.First().Name);
}
And pertinent View markup:
#model REOModern.Models.CriteriaModel
...bunch of HTML...
#Html.LabelFor(model => model.Subdivision)
#Html.DropDownListFor(x => x.Subdivision, new SelectList(Model.Subdivision, "ID", "Name", Model.Subdivision.First().ID))
...other HTML...
<button type="submit" class="btn btn-primary" value="Index">Search</button>
I should clarify: I know that my 'return View("Search obtained" + ...' will fail, but it should show the piece of data that I need. The problem is it's a null reference exception. Until I can fix that, there's no point in building a user-friendly View for submitted search criteria.
MVC does not repopulate the List<> elements.
You would split the selected value out into another property of the model.
So in your model, include something like this
public int SelectedValue { get; set; }
Then for your Html.DropDownListFor helper you would use
Html.DropDownListFor(model => model.SelectedValue, Model.DropDownList, new { /* htmlAttributes */ });
Of course they're empty. The only data that exists in your post action is that which was posted via the form. Since the entire dropdown list, itself, was not posted, merely a selected item(s), the lists are empty. For anything like this, you need to rerun the same logic in your post action to populate them as you did in your get action. It's usually better to factor out this logic into a private method on your controller that both actions can use:
private void PopulateSomeDropDownList(SomeModel model)
{
// logic here to construct dropdown list
model.SomeDropDownList = dropdownlist;
}
Then in your actions:
PopulateSomeDropDownList(model);
return View(model);

Keep a value in a ViewModel even if the value is not modified

In a C# MVC 5 Internet application, I have a HTTP Get Edit action result, that gets an object, and places this object in a ViewModel and this is then displayed in a View.
One of the fields in the ViewModel is a value that is not edited in the view. In the HTTP Post Edit action, the value that is not edited in the view has been reset.
How can I keep this value so that it is the same value in the HTTP Post method as the HTTP Get method?
Thanks in advance
EDIT
Here is the ViewModel code:
public class MapLocationViewModel
{
[Editable(false)]
public int mapCompanyForeignKeyId { get; set; }
public MapLocation mapLocation { get; set; }
}
Here is the code at the bottom of the HTTP Get Edit Action result, where the mapCompanyForeignKeyId is set:
MapLocationViewModel mapLocationViewModel = new MapLocationViewModel();
mapLocationViewModel.mapLocation = maplocation;
mapLocationViewModel.mapCompanyForeignKeyId = maplocation.mapCompanyForeignKeyId;
return View(mapLocationViewModel);
Here is the HTTP Post Edit Action result code:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(MapLocationViewModel mapLocationViewModel)
In the above HTTP Edit Action result code, the mapLocationViewModel.mapCompanyForeignKeyId is reset to 0, after this value has been set to a number in the HTTP Get Edit Action result.
You should try make hidden input. With Razor syntax it would be:
#using (Html.BeginForm())
{
#Html.HiddenFor(model => model.YourProperty)
}
YourProperty will not be visible but it's value will be in the view model sent to the POST method.
You can also use HiddenInputAttribute for this:
[HiddenInput(DisplayValue=false)]
public int YourProperty {get; set;}
If you are using the #Html.TextboxFor(m => m.MyField) or similar helpers within a form, by default, it should automatically spit out all the existing values for each field and thus you should see all values whether modified or not. When it is posted, each included field will be serialized. If you use the helpers, you won't have to worry about naming convention as Razor and the model binder will do the work for you.
Check the request coming into your POST action to see if it is a model binding issue or a client issue. If you don't see the desired members in the body (or query string, if a GET) then you must not be sending them from the client, which can be due to improper serialization/naming of fields, not including the field in the page, not sending the value of the field to the page, or including the field outside of the form, among other reasons...
Example:
public class MyViewModel
{
[Required]
public string Field1 { get; set; }
[Required]
public string Field2 { get; set; }
}
...
#model MyViewModel
#using (Html.BeginForm("MyAction", ...)
{
#Html.LabelFor(m => m.Field1)
#Html.TextboxFor(m => m.Field1)
<br />
#Html.LabelFor(m => m.Field2)
#Html.TextAreaFor(m => m.Field2)
<button type="submit">Submit</button>
}
...
[HttpPost]
public ActionResult MyAction(MyViewModel model)
{
if (!ModelState.IsValid)
return MyGetAction(model);
...
}

null on HTTPPost on ASP.NET

My Problem is like this ,Im trying to get a model object from a view after seinding it with a form,the model Looks like this:
public class PackageModel
{
public PackageDTO Package { get; set; }
public IEnumerable<SelectListItem> Allcategories { get; set; }
}
while PackageDTO is just an DTO object conatining many attributes.
Now the view for this model,ist just showing the attributes and this model will be sent within a httppost request to the index page as normal(there it will be processed ans saved ),
the index method in the Controller Looks like this:
[HttpPost]
public ActionResult Index(PackagemODEL packageModel, FormCollection form)
{
}
Now i dont know what im doing wrong,but the Object packageModel is not totally null,just the list Allcategories and another string Attribute in the PackageDTO object,the rest seems to be working.
The view contains this code
<fieldset>
<legend>#Resources.AppvManagementService_EditPackage_Title</legend>
#using (Html.BeginForm("Index","WantedController",FormMethod.Post,new {enctype="multipart/form-data"}))
{
#Html.ValidationSummary()
<labelName </label>#Html.TextBoxFor(model=>model.Package.Name) <br/>
<label>Sid </label>#Html.TextBoxFor(model=>model.Package.Sid,new {#disabled="disabled"}) <br/>
<label>Category </label>#Html.DropDownList("CategoryName",Model.Allcategories,Model.Package.Category)<br/>
<label>Description: </label>#Html.TextBoxFor(model=>model.Package.Description) <br/>
<label>Type: </label>#Html.TextBoxFor(model=>model.Package.Type) <br/>
<button type="submit">submit</button>
}
Doest anyone have any idea why ist like this?? am i doing something wrong(im sure i am :))
thx for every one
How do you expect Allcategories to be populated? Your view contains a field, which posts a value under the name "CategoryName" - there's nothing in your view that populates a list of categories. More importantly; to you really need it to be populated? It seems to me that Allcategories is only really needed for populating the dropdown in the view. On the post, you shouldn't need it. If you DO still need it, you're going to have to either:
Repopulate it in the controller on the HttpPost method:
[HttpPost]
public ActionResult Index(PackagemODEL packageModel, FormCollection form)
{
packageModel.Allcategories = new IEnumerable<SelectListItem>();
}
Clutter up your view with pointless hidden fields to pass the values back in (I wouldn't recommend this for a list of items unless you really need to):
#for (int i = 0; i < Model.Allcategories.Count; i++)
{
#Html.HiddenFor( m => m.Allcategories[i])
}
Populate it in the model constructor:
public class PackageModel
{
public IEnumerable<SelectListItem> Allcategories { get; set; }
public PackageModel()
{
Allcategories = new IEnumerable<SelectListItem>();
/* Add values to Allcategories here */
}
}
If the values of Allcategories doesn't change, you could also consider making it a static readonly property of your model and hardcoding the values (or pulling them from a config file or similar).
As for getting back the selected CategoryName, you need a field in your model in which to store it, otherwise the only way to access it at the moment is via Request.Form:
public class PackageModel
{
public IEnumerable<SelectListItem> Allcategories { get; set; }
public string CategoryName { get; set; }
}
#Html.DropDownListFor(m => m.CategoryName, Model.Allcategories, Model.Package.Category)
An aside: Please, please, please take your DTO out of your model and set appropriate properties in your model itself. Your DTO does not belong in your view model, which is a model for your view and nothing more.

How to use multiple form elements in ASP.NET MVC

So I am new to ASP.NET MVC and I would like to create a view with a text box for each item in a collection. How do I do this, and how do I capture the information when it POSTs back? I have used forms and form elements to build static forms for a model, but never dynamically generated form elements based on a variable size collection.
I want to do something like this in mvc 3:
#foreach (Guest guest in Model.Guests)
{
<div>
First Name:<br />
#Html.TextBoxFor(???) #* I can't do x => x.FirstName here because
the model is of custom type Invite, and the
lambda wants to expose properties for that
type, and not the Guest in the foreach loop. *#
</div>
}
How do I do a text box for each guest? And how do I capture them in the action method that it posts back to?
Thanks for any help.
Definitely a job for an editor template. So in your view you put this single line:
#Html.EditorFor(x => x.Guests)
and inside the corresponding editor template (~/Views/Shared/EditorTemplates/Guest.cshtml)
#model AppName.Models.Guest
<div>
First Name:<br />
#Html.TextBoxFor(x => x.FirstName)
</div>
And that's about all.
Now the following actions will work out of the box:
public ActionResult Index(int id)
{
SomeViewModel model = ...
return View(model);
}
[HttpPost]
public ActionResult Index(SomeViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// TODO: do something with the model your got from the view
return RedirectToAction("Success");
}
Note that the name of the editor template is important. If the property in your view model is:
public IEnumerable<Guest> Guests { get; set; }
the editor template should be called Guest.cshtml. It will automatically be invoked for each element of the Guests collection and it will take care of properly generating ids and names of your inputs so that when you POST back everything works automatically.
Conclusion: everytime you write a loop (for or foreach) inside an ASP.NET MVC view you should know that you are doing it wrong and that there is a better way.
You can do this:
#for (int i = 0; i < Model.Guests.Count; i++) {
#Html.TextBoxFor(m => m.Guests.ToList()[i].FirstName)
}
There are more examples and details on this post by Haacked.
UPDATE: The controller post action should look like this:
[HttpPost]
public ActionResult Index(Room room)
{
return View();
}
In this example I'm considering that you have a Room class like this:
public class Room
{
public List<Guest> Guests { get; set; }
}
That's all, on the post action, you should have the Guests list correctly populated.

Categories