How to properly preserve view rendering data on model validation failure? - c#

In my application, I have a form where a user can add a bank account to their account. I've implemented this through two actions on my controller, namely:
public async Task<IActionResult> Add()
{
var client = await _clientFactory.CreateClientAsync();
// We obtain the available account types from a remote service.
var accountTypes = await client.GetAccountTypesAsync();
var viewModel = new AddAccountViewModel
{
AccountTypes = accountTypes;
}
return View(viewModel);
}
[HttpPost]
public async Task<IActionResult> Add(AddAccountViewModel model)
{
if (!ModelState.IsValid)
return View(model);
// Store the account for the user, and redirect to a result page.
}
My AddAccountModel currently looks like this, and has two purposes:
public class AddAccountViewModel
{
public List<AccountTypeModel> AccountTypes { get; set; }
[Required]
[StringLength(100)]
public string AccountName { get; set; }
[Required]
[Range(1, int.MaxValue]
public decimal CurrentBalance { get; set; }
[Required]
public Guid AccountTypeId { get; set; }
}
One the one hand, the AccountTypes property is used to populate a list in the view:
#{
var accountTypeList = new SelectList(accountTypes, "Id", "Name");
}
<select class="form-control" id="accountType" asp-for="AccountTypeId" asp-items="#accountTypeList">
</select>
This all works fine when I arrive on the initial page via GET /Add. The form renders, the select box is populated with the items from the service, as I'd expect.
However, the form also includes validation, and should a user input invalid data or leave fields empty, POST /Add is called, passed the model, runs validation, and ultimately redirects the user back to GET /Add, with the specific reasons as to why validation failed. This is where the problem arises.
When the user is redirected back to the form and asked to correct their input, AccountTypes in my model is null. Which makes sense, as we didn't send it as a part of the POST request - it only serves as input for the form to render. With the form not knowing the account types, the user will never be able to correct their input, as they won't be able to select a valid account type.
As mentioned above, my model is two-fold: on the one hand it contains data the view requires to render, and on the other hand it contains data the user provides and is then processed by the application.
Given the above, my question is: how should I properly preserve the input data required for my view to render when model validation failed? I.e., is my ViewModel the correct place to keep the collection of AccountTypeModels, or should I be keeping them in ViewData, or perhaps even TempData?
Currently, both my application and services are stateless, which is why I didn't go down the "just stick it in TempData" route yet, as from what I can gather, TempData is stored in the server's session. I also don't want to query the service for the available account types at the start of the view if I can avoid it, as it seems to violate MVC's separation of concerns.
Thanks for reading, and I hope someone can help me out with this.

You need to repopulate the AccountTypes collection in the POST method before you return the view. I would recommend that your view model contain a property public IEnumerable<SelectListItem> AccountTypes { get; set; } rather than List<AccountTypeModel> so that you do not need to generate the SelectList in the view.
To keep it DRY, create a private method to configure your view model
public async Task<IActionResult> Add()
{
var model = new AddAccountViewModel();
ConfigureViewModel(model);
return View(model);
}
[HttpPost]
public async Task<IActionResult> Add(AddAccountViewModel model)
{
if (!ModelState.IsValid)
{
ConfigureViewModel(model);
return View(model);
}
....
}
private async Task ConfigureViewModel(AddAccountViewModel model)
{
var client = await _clientFactory.CreateClientAsync();
var accountTypes = await client.GetAccountTypesAsync();
model.AccountTypes = new SelectList(accountTypes, "Id", "Name");
}

Related

MVC - How to back to previous page without refresh the textbox into blank

I am using ASP.NET MVC to develop my website, however now I am doing my registration page. I do a POST to my action method and I want to check whether the email is still valid. If the email is not valid, the action returns the view again, but the form is reset (all form data is gone). How can I keep the form data without refresh?
Imagine you have a model like this:
public class RegistrationModel
{
[EmailAddress]
public int Email { get; set; }
// Other properties
}
Then when the person requests the registration page, you will serve the page using an action like this:
[HttpGet]
public ActionResult Register()
{
var model = new RegistrationModel();
return View(model);
}
Your view needs to accept that model. Then the view (page) will be sent to the browser. The user will fill it out and send it back by submitting the form so you need another action to handle the submission. That action may look like this:
[HttpPost]
public ActionResult Register(RegistrationModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// model is valid so do whatever you need to
// such as saving to db etc.
// Here do a redirect to some other page
return RedirectToAction("Index", "Home");
}
See in the above action, if the model is not valid, it sends the model to the view. The model has the email the user had entered so it will render it in the textbox and then send it back to the user.
EDIT
I added [EmailAddress] attribute to the model. To use it make sure you import the namespace System.ComponentModel.DataAnnotations. There are many other attributes you can use such as [Required] etc. The full list is here.

ASP.NET MVC - Proper usage of View Model and Command pattern

I've been writing ASP.NET MVC applications for some time and I found them to be a good place for using the command pattern: we represent every user request as a command - a set of input params - then this command is processed (processing includes validation and other domain logic) and the result is sent back to the user.
Another thing I've been using in my applications is view models. I found them to be a more convenient way of passing data to the view than using domain objects as models or filling ViewData/ViewBag.
These 2 concepts work great for separating data that is shown to the user from user input and its handling, but they don't quite agree with each other in ASP.NET MVC.
Let's say I want to use commands and view models when developing a simple web store where users look through products and can order a product by providing their name and email address:
class ProductViewModel
{
public ProductViewModel(int id) { /* init */ }
public int Id { get; set; }
public string Name { get; set; }
// a LOT of other properties (let's say 50)
}
class OrderProductCommand
{
public int ProductId { get; set; }
[Required(ErrorMessage = "Name not specified")]
public string Name { get; set; }
[Required(ErrorMessage ="E-Mail not specified")]
public string Email { get; set; }
public CommandResult Process() { /* validate, save to DB, send email, etc. */ }
}
When looking through tutorials and SO I've seen people suggest several ways of doing this.
Option 1
Controller:
[HttpGet]
public ActionResult Product(int id)
{
return View(new ProductViewModel(id));
}
[HttpPost]
public ActionResult Product(OrderProductCommand command)
{
if (ModelState.IsValid)
{
var result = command.Process();
if(result.Success)
return View("ThankYou");
else
result.CopyErrorsToModelState(ModelState);
}
return Product(command.Id);
}
View:
#using (Html.BeginForm())
{
#Html.Hidden("ProductId", Model.Id)
#Html.TextBox("Name")
#Html.TextBox("Email")
<input type="submit" value="Place order" />
}
Pros: view model and command are separated from each other, the HttpPost method looks clean
Cons: I can't use convenient HTML helpers like #Html.TextBoxFor(model => model.Email), I can't use client validation (see my other question)
Option 2
We copy Id, Name and Email together with their validation attributes from command to viewModel.
Controller:
[HttpPost]
public ActionResult Product(ProductViewModel viewModel)
{
var command = new OrderProductCommand();
command.Id = viewModel.Id;
command.Name = viewModel.Name;
command.Email = viewModel.Email;
if (ModelState.IsValid)
// ...
}
View:
#Html.TextBoxFor(m => m.Email)
...
Pros: all of option 1 cons go away
Cons: copying of properties seems inconvenient (what if I have 50 of them?), validation of Name and Email in view model (it should be done in command where the rest of the domain logic resides), model as a POST parameter (see below)
Option 3
We make command a property of viewModel.
Controller:
[HttpPost]
public ActionResult Product(ProductViewModel viewModel)
{
var command = viewModel.Command;
if (ModelState.IsValid)
// ...
}
View:
#Html.TextBoxFor(m => m.Command.Email)
...
Pros: all of option 1 cons go away
Cons: view model should only contain data that is displayed to the user (and command is not displayed), model as POST parameter (see below)
--
What I don't like about options 2 and 3 is that we use a view model as a POST method parameter. This method is meant for handling user input (only 2 fields + 1 hidden in this case) and the model contains 50 more properties that I'll never use in this method and that will always be empty. Not to mention the necessity to create an empty constructor for the view model just to handle this POST request and the unnecessary memory consumption when creating large view model objects for every POST request.
My question is (that's like the longest question ever, I know): is there a secret Option 4 for properly using commands and view models that has all of the pros and none of the cons of the other ones? Or am I being paranoid and these cons are not that important and can be ignored?
Seems like the only other decent way go is to use a partial view for rendering the form and use OrderProductCommand as the view model.
Product.cshtml:
#model ProductViewModel
...
#Html.Partial("Product_OrderForm", new OrderProductCommand { ProductId = Model.Id })
...
Product_OrderForm.cshtml:
#model OrderProductCommand
...
#using (Html.BeginForm("Product", "Home"))
{
#Html.HiddenFor(cmd => cmd.ProductId)
#Html.TextBoxFor(cmd => cmd.Name)
#Html.TextBoxFor(cmd => cmd.Email)
<input type="submit" value="Place order" />
}
...
This way there is no need to create a data map between view models and business objects, and the controller code can be left clean as it was in in Option 1:
[HttpGet]
public ActionResult Product(int id)
{
return View(new ProductViewModel(id));
}
[HttpPost]
public ActionResult Product(OrderProductCommand command)
{
// process command...
}
Personally,
If I had to pass my model back to the view using a viewModel, I'd use option 4, inherit my view model from my command.
That way I get all the properties for my command, and I can set new properties that are just needed for the view, say dropdown list options etc.
Let inheritance do the work for you.
Also, you don't need to copy properties, in your post, don't send back a ViewModel, send back the command.
public ActionResult Product(PreOrderProductCommand command)
Don't forget, Mvc doesn't care what model is in your view, it only maps keys on the formcollection to properties in the model in the argument list. So even though you send a ProductViewModel out, you can still get a PreOrderProductCommand in.
HTH
Here's my take on the issue.
The reason we introduce all these layers (entity, view model, command etc.), which essentially represent the same concept within different domains, is to enforce separation of concerns.
However, as each layer is introduced, we increase the complexity and margin for error, due to increased mapping between objects and distributed validation.
In my opinion it is absolutely correct that entities and view models are implemented separately; a domain entity should represent the business logic, and shouldn't be polluted with UI specific features. Similarly, a view model shouldn't contain more than is necessary than to satisfy a specific view.
There is no reason, on the other hand, that commands should introduce a new layer to your architecture. A command simply needs to provide data, doesn't need to rely on a specific implementation, and therefore can be defined as an interface:
interface IOrderProductCommand
{
int ProductId { get; }
string Name { get; }
string Email { get; }
}
Whilst a view model isn't a command, or vice-versa, a view model could act as a command:
class ProductViewModel : IOrderProductCommand
{
public int ProductId { get; set; }
[Required(ErrorMessage = "Name not specified")]
public string Name { get; set; }
[Required(ErrorMessage ="E-Mail not specified")]
public string Email { get; set; }
public ProductViewModel(int id) { /* init */ }
// a LOT of other properties (let's say 50)
}
This way, the validation takes place only in the view model, which I think is the correct place for that to happen, as feedback can be given to the user immediately.
The command should simply transfer data, and the domain entity it mutates should validate itself anyway; a third layer of validation is unnecessary.
Your controller would then look as follows:
readonly CommandHandler _handler;
public YourController(CommandHandler handler)
{
_handler = handler;
}
[HttpGet]
public ActionResult Product(int id)
{
return View(new ProductViewModel(id));
}
[HttpPost]
public ActionResult Product(ProductViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
_handler.HandleProductCommand(model);
return RedirectToAction(nameof(Product), new { id = model.ProductId });
}
And the handler:
class CommandHandler
{
void HandleProductCommand(IOrderProductCommand command)
{
// Update domain...
}
// Other command handling methods...
}

MVC structure query

Evening all, I'm chasing my tail trying to get the structure of a simple ASP.NET MVC correct. From the start I should say I'm completely new to ASP and MVC, I've used a bit C# before.
For examples sake let's say I'm trying to check two pieces of information e.g. a username and password that a user enters, against a database that stores all the user info. If they put in the correct credentials a summary of the user info is displayed, if not they are taken back (well visually never leave) the login page with a please try again message.
So I have my Home (Login page) view, a HomeController and a HomeIndexViewModel. Similarly I have an Account view, a AccountController and an AccountUserViewModel (there is also AccountIndexViewModel but that isn't really used).
The Home view 'takes in' (through it's controller) a HomeIndexVM as it's model:
#model ViewModels.HomeIndexViewModel
#using (Html.BeginForm("User", "Account", FormMethod.Post))
{
if (#Model.PreviousAttempts)
{
<p><b>Username or password were not recognised please try again.</b></p>
}
<p>Username: #Html.TextBoxFor(x => x.Username)</p>
<p>Password: #Html.PasswordFor(x => x.Password)</p>
<input id="btnLogin" type="submit" value="Login" />
}
HomeController:
public class HomeController : Controller
{
public ActionResult Index(bool invalidLogin = false)
{
var vm = new HomeIndexViewModel() { Username = string.Empty, Password = string.Empty, PreviousAttempt = invalidLogin };
return View(vm);
}
}
And finally the HomeIndexViewModel.cs
public class HomeIndexViewModel
{
public string Username { get; set; }
public string Password { get; set; }
public bool PreviousAttempt { get; set; }
}
I think that is okay so far. Now on clicking the login button, it will post to Account, User.
public ActionResult User(UserLogin userLogin)
{
if (!ModelState.IsValid)
return RedirectToAction("Index", "Home", new { invalidLogin = true });
// Match username and password against database
User user = userLogin.IsValid(userLogin.Username, userLogin.Password);
if (user != null)
{
return this.View(user);
}
return RedirectToAction("Index", "Home", new { invalidLogin = true });
}
There is a couple of things here, you can see the redirects back to the login page with the true flag to show the failed login message.
But more to my point, clearly it won't work as it takes a UserLogin as a parameter. This is a model object that contains:
public class UserLogin
{
private NewDBSolution_v1Entities accountsDB = new NewDBSolution_v1Entities();
[Required, MinLength(2)]
public string Username { get; set; }
[DataType(DataType.Password), Required]
public string Password { get; set; }
public bool PreviousAttempts { get; set; }
public User IsValid(string username, string password) // Ideally use the local username and password properties rather than pass in as they are the same.
{
// Match username and password against database and return full user info if match found, otherwise return null
}
}
So what am I asking... well is it best practice for, in this case, the Account's User Action to take the HomeIndexViewModel, even although it's Home as opposed to Account related? Or should I pass in a model object as I had originally done, and use it to the validation (what I don't like about this is the validation is done in object that is passed from a different view if that makes sense?)
How do you best bundle information up from a view to pass to an Action? I realize VMs and Model objects the compiler doesn't care they are just classes but I'd like to get my separation of concerns correct.
Basically here all I need is the username and password from the Home view, should that be bundled in to a VM or a M?
It just seems to me there is potential for an awful lot of classes that are just slightly different, so why not make one and use it. I suppose that is where inheritance comes in, but do you gain much there if ever subclass just adds one different property?
Anyway I keep going in circles about the best way to structure. I did read somewhere that VMs should basically be a mask/adapter over the model such that the view only sees just what it needs. But these VMs don't have a model associated with them.
I'm rambling now, if anyone can make head or tail of this and give me a few pointers I'd be very grateful, thanks.
I think you might be going round in circles because you've created the login view in the HomeController. Login code is Account-related, so why not put this into the AccountController instead?
I tend to favour posting to the same action as you get from, if you follow the Post-Redirect-Get pattern, as you're trying to do here. I would move the login action into the AccountController. I would move the code that does the database check into a separate class, rather than keeping it in the model. Perhaps a membership provider or something like that. I would pass that provider into the Controller - this allows the Controller and the Model object to stop worrying about how to decide whether a user is valid or not. They can just ask the provider to tell them.
It's generally good practice to keep your entities (whether UI or domain) simple. They certainly should have database connection objects inside them.
Your Controller would look something like this:
public class AccountController : Controller
{
private readonly IMembershipProvider membershipProvider;
public AccountController(IMembershipProvider membershipProvider)
{
this.membershipProvider= membershipProvider;
}
public ActionResult Login()
{
var viewModel = new LoginViewModel();
return View(viewModel);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel viewModel)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
var user = membershipProvider.Find(viewModel.Username, viewModel.Password);
if (user != null)
{
membershipProvider.SignIn(user, true);
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("Login", "Your credentials were not recognised. Please try again.");
return View(viewModel);
}
}
Because the Login action has the same name for both Get and Post, your view becomes simpler. You'd have a Login.cshtml view in the Views/Account folder like this:
#model ViewModels.LoginViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken() // good practice to add this, ties in with the ValidateAntiForgeryToken attribute on the Post action
#Html.ValidationSummary() // displays model errors if there are any
<p>Username: #Html.TextBoxFor(x => x.Username)</p>
<p>Password: #Html.PasswordFor(x => x.Password)</p>
<input id="btnLogin" type="submit" value="Login" />
}
To answer this specific question:
How do you best bundle information up from a view to pass to an Action?
You let the default model binder take care of that if you can. It's very powerful and can handle most situations in regards to data being passed back from a form. All the model binder does is match name-value pairs from the form collection that is passed back when the form is posted to properties on your class. Provided your class has the string properties Username and Password the model binder will populate them. The actual class that you provide is irrelevant as far as the model binder is concerned. This would work equally well (but anyone else working on the project wouldn't thank you for it!):
public class Dave
{
[Required, MinLength(2)]
public string Username { get; set; }
[DataType(DataType.Password), Required]
public string Password { get; set; }
}
And then in the Post action:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(Dave dave)
{
...
}
Does that help?

Create (not read) field values into a new view in C# MVC

I've looked, tried several different solutions and haven't found anything that works (at least, not something with an example close enough to what I want for me to follow). I'm sure I'm missing something that would be a simple thing to a more experienced coder. Help?
I have a Model called Residents. It includes ResidentID, PFName, PLName. I have a controller for Residents. I have CRUD views for Residents. All working just fine.
I have a Model called Logs. It includes LogID, ResidentID, Comments. I have a controller for Logs. I have CRUD views for Logs. All working just fine.
I can display all the log entries for a Resident. Works fine. After a Log entry has been created, I can display the PFName using the method
#Html.DisplayFor(model => model.Resident.PFName)
Next, I want to Create a new log entry for a selected Resident.
That's where I'm having the problem. I would like the "Create" view (for the Log) to display the ResidentFName and ResidentLName of the selected resident, not the ResidentID.
A this point, from the Details view for a Resident, I have a CreateLog link.
#Html.ActionLink("New Log Entry", "../Log/Create", new { #ResidentID = Model.ResidentID})
This (likely not the best way) gives me a URL with the value of the selected ID
http://localhost:999/Log/Create?ResidentID=1
The value for the ResidentID is correct; it changes depending on which Resident is selected.
This value is correctly entered
#Html.TextBoxFor(model => model.ResidentID)
on the new CreateLog page using the Log Controller Create action.
public ActionResult Create(int ResidentID)
I plan to hide the ResidentID TextBox so the user doesn't see it. It seems I have to make it available in the form to be able create a new log entry.
The CreateLog form currently works as I have it now. I can create a log entry and verify that entry has been correctly recorded for the Resident.
But, I would like the form to display the PFName and PLName for the Resident so the user has visible feedback for which Resident was selected.
I believe that the related data (PFName and PLName) I want has to be passed to the CreateLog form .... somehow. I can't get it from the form.
Since there's only the unsaved entry for ResidentID, I can't use the value from the CreateLog form it to display related data. As mentioned, for the Lists, there is no such problem. It's only for CreateLog.
I've tried adding the data to the URL. Not working. I've tried setting the strings in the Controller (and the URL). Not working. I've looked at setting a cookie, but haven't ever done that so not sure what to set or where to put it or how to get the values from it. I've looked at setting a variable in the controller ... (have that working to display drop down lists, but a list to select from is not what I need -- I want the matching values from the related table).
Log.LogID(PK, Identity)
Log.ResidentID(FK)
Resident.PFName
Resident.PLName
I can directly create a view with these tables/fields in my SQLDB and update it.
Assuming a view model which looks something like this:
public class CreateLogViewModel
{
public int ResidentID { get; set; }
public string PFName { get; set; }
public string PLName { get; set; }
public string SomeLogCreationProperty { get; set; }
// other properties
}
Your controller could look something like this:
public ActionResult Create(int ResidentID)
{
var model = db.Residents.Where(r => r.ResidentID == ResidentID)
.Select(r => new CreateLogViewModel
{
ResidentID = r.ResidentID,
PFName = r.PFName,
PLName = r.PLName
// other properties
});
return View(model);
}
Then the view:
#model CreateLogViewModel
#using (Html.BeginForm())
{
#Html.HiddenFor(m => m.ResidentID)
#Html.HiddenFor(m => m.PFName)
#Html.HiddenFor(m => m.PLName)
#Html.EditorFor(m => m.SomeLogCreationProperty)
// other properties
<input type="submit" />
}
This would then POST back to:
[HttpPost]
public ActionResult Create(CreateLogViewModel model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Index");
}
// Redisplay the form with errors
return View(model);
}
Expanding on John H and StuartLC answers, you need to use ViewModels and the following workflow:
Database->(load)->Model->Controller->(convert)->ViewModel->View
and
View->ViewModel->Controller->(convert)->Model->(save)->Database
So lets says you have the following models:
namespace Models
{
public class Residents
{
public int ResidentID { get; set; }
public string PFName { get; set; }
public string PLName { get; set; }
//...
}
public class Logs
{
public int LogID { get; set; }
public int ResidentID { get; set; }
public string Comments { get; set; }
//...
}
}
You need a ViewModel that combines the data you need for display and input in your Log\CreateView:
namespace ViewModels
{
public class ResidentLog
{
public int ResidentID { get; set; }
public string PFName { get; set; }
public string PLName { get; set; }
public string Comments { get; set; }
//...
}
}
Then inside the controller:
public class LogController : Controller
{
[HttpGet]
public ActionResult Create(int ResidentID)
{
// Run in debug and make sure the residentID is the right one
// and the resident exists in the database
var resident = database.Residents.Find(residentID);
var model = new ViewModels.ResidentLog
{
ResidentID = resident.ResidentID,
PFName = resident.PFName,
PLName = resident.PLName,
Comments = string.Empty,
// ...
};
// Run in debug and make sure model is not null and of type ResidentLog
// and has the PFName and PLName
return View(model);
}
[HttpPost]
public ActionResult Create(ViewModels.ResidentLog model)
{
if (!ModelState.IsValid)
return View(model);
var log = new Models.Logs
{
// Assumes LogID gets assigned by database?
ResidentID = model.ResidentID,
Comments = model.Comments,
};
// Run in debug and make sure log has all required fields to save
database.Logs.Add(log);
database.SaveChanges();
return RedirectToAction("Index"); // Or anywhere you want to redirect
}
}
Then your Log\CreateView:
#model ViewModels.ResidentLog
<!-- Display the values needed -->
<div>#Model.ResidentID - #Model.PFName - #Model.PLName</div>
#using (var form = Html.BeginForm(...))
{
<!-- This saves the values for the post, but in fact only ResidentID is actually used in the controller -->
#Html.HiddenFor(m => m.ResidentID)
#Html.HiddenFor(m => m.PFName)
#Html.HiddenFor(m => m.PLName)
#Html.EditorFor(m => m.Comments)
<input type="submit" />
}
You need to provide the additional information to the view.
This can be done in at least 2 ways
Use the ViewBag dynamic as a quick and dirty cheap and cheerful container to pass everything the view needs from the controller.
(preferred) Use a custom ViewModel with a tailor made class which holds everything the view needs. This is generally preferred as it is statically typed.
(I'm assuming that resident is already persisted in the database by the time the Log controller is called - you might need to fetch it elsewhere)
So, in your log controller, here's an example of using ViewBag:
[HttpGet]
public ActionResult Create(int residentID)
{
ViewBag.Resident = Db.Residents.Find(residentId);
return View();
}
You can then show the resident properties on the view by utilizing the ViewBag.
Edit
Yes, by persisted I meant in the Db - apologies about using unclear jargon.
Here's another example of ViewBag approach (the idea is to create a new Comment for another object):
Doing this the cheap + cheesy ViewModel way - in the HTTPGet Controller Create method:
public ActionResult Create(string objectType, int objectId)
{
// This is equivalent to youn fetching your resident and storing in ViewBag
ViewModel.Object = FetchSomeObject(objectType, objectId);
return View();
}
And in the View I use this (The ViewBag is accessible to Controller and View):
<title>#string.Format("Add new Comment for {0} {1}", ViewBag.Object.ObjectType, ViewBag.Object.Name);</title>
As you say, you will also need to do add a hidden for the ResidentId in your create log form
As per #JohnH's answer (+1), the BETTER way to do this (than using the magic ViewBag dynamic) is to create a custom ViewModel specifically for this screen. The ViewModel can either be reused both ways (GET: Controller => View and POST : Browser => Controller, or you even have separate ViewModels for the Get and Post legs.
With much thanks to all, I have it working. The final piece was telling the controller to return the model (nl). Here's the full spec for what's working:
I have created a ViewModel that includes
public class NewLog
{
public int ResidentID { get; set; }
public string PFName { get; set; }
public string PLName { get; set; }
public string Comment { get; set; }
// other properties
}
In the LogController,
public ActionResult Create(int ResidentID)
{
var resident = db.Residents.Find(ResidentID);
var nl = new NewLog
{
ResidentID = ResidentID,
PFName = resident.PFName,
PLName = resident.PLName,
Comment = string.Empty,
};
return View(nl);
}
In the Create.cshtml page,
#model My.Models.NewLog
The required ResidentID to be recorded with the new Log Entry
#Html.TextBoxFor(model => model.ResidentID, new {#Type = "Hidden"})
And the related, user-friendly display boxes for the person's name
#Html.DisplayFor(model => model.PFName)
#Html.DisplayFor(model => model.PLName)
And in the URL which is used to access the create page,
#Html.ActionLink("New Log Entry", "../Log/Create", new { #ResidentID = item.ResidentID, item.PFName, item.PLName})

Display text value of ASP.NET MVC SelectList on following page

I am working on a website that allows people to register for events. On the registration page, I have a form that captures the data, validates in an MVC controller, persists it between Views, and then is supposed to display a preview page.
Below is a simplified example of what I am trying to do.
Person ViewModel
public class Person{
public int TitleId {get; set;}
public SelectList Titles {get; set;}
}
Register Controller
public class RegisterController:Controller
{
[HttpGet]
public ActionResult Register(){
return View(new Person());
}
[HttpPost]
public ActionResult Register(Person person){
if(ModelState.IsValid){
TempData["Person"] = person
RepopulateSelectLists(); //Gets the data for the select list again
return RedirectToAction("Preview");
}
return View();
}
[HttpGet]
public Actionresult Preview(){
Person person = (Person)TempData["Person"];
}
}
Preview View
#Html.DisplayFor(m => m.TitleId); //Will display int, not selected value
Assuming my SelectList has all of the values, and I have the key/Id of the value selected on the previous page, how do I redisplay just the text of what they selected?
It doesn't work that way. In your Preview() page you'd take the given ID and find its associated name. You'll likely want to build a custom View Model (VM) in this case to keep the ID and the Name in the VM.
Also, the moment you hit F5 on the preview page, everything will be GONE. TempData doesn't persist and any refreshing of the page will lose the data.
I would recommend, instead, that on successful postback, you just return a different view based on the initial post data. For example:
[HttpPost]
public ActionResult Register(Person person){
if(ModelState.IsValid){
RepopulateSelectLists(); //Gets the data for the select list again
var previewVM = new PreviewVM();
// populate it with the model or whatever else you need for the preview
return View("Preview", previewVM);
}
return View();
}
An example VM of a person
public class Person { public int TitleId { get; set; } }
public class PersonVM : Person { public string TitleName { get; set; } }
// ... then in your Register method
var previewVM = new PersonVM { TitleId = person.TitleId, TitleName = GetTitle(person.TitleId) };
Assuming GetTitle does a lookup based on the TitleID. It's very coarse code, but I hope you get the gist of it. THen you'd use Html.DisplayFor(m => m.Title); And if your preview has a post to the next page, you'd want to have Html.HiddenFor(m => m.TitleId) to make sure the value passes on but you'd have to do that even with your current method. Alternatively you could store the Title in the ViewBag (or ViewData -- same thing) and simply not useDisplayForto display the value and pass in the value from theViewBag`
Notice I scrapped the "Preview" method completely. That way, if you hit F5 at this point, it just re-posts with the same data you used to get to the preview page. And assuming you're not saving (I don't see where you are) then there's no harm in posting back. Make sense?

Categories

Resources