I've seen a couple possible solutions but they look very messy to me. Does anyone have a simple solution for this?
Model:
public class MyClass
{
public KeyValuePair<int,string> Field { get; set; }
}
Get method in controller:
public ActionResult Index()
{
var model = new MyClass();
model.Field = new KeyValuePair<int, string>(1, "test");
return View(model);
}
View:
#model WebApplication1.Models.MyClass
#using (Html.BeginForm("MyMethod", "Home", FormMethod.Post))
{
#Html.Hidden("Field", Model.Field);
<input type="submit" value="Submit" />
}
Post method in controller:
public ActionResult MyMethod(MyClass input)
{
var x = input.Field;
....
}
The Key Value pair is not passed with this method as it just comes up empty. What would be the easiest way of getting 'Field' passed to the controller?
Replace following line and try it.let me know any problem.
#Html.Hidden("Field", Model.Field);
TO
#Html.HiddenFor(m => m.Field);
If memory serves correctly, it has a Key and Value property, which like any other object would be bound like:
#Html.HiddenFor(m => m.Field.Key)
#Html.HiddenFor(m => m.Field.Value)
One input field can't represent the key and value in this way, unless you look at using a Custom Model Binder to manage the value posted back, and a Display/Editor template to render the input in a way the model binder will understand.
Following Brians answer, I've had to accept I must do it like this:
Class:
public class MyClass
{
public KeyValuePair<int,string> Field { get; set; }
public string FieldKey { get; set; }
public string FieldValue { get; set; }
}
View:
#Html.Hidden("FieldKey",Model.Field.Key)
#Html.Hidden("FieldValue", Model.Field.Value)
It will get messy as I have many of these kvp's I'd like to pass back to the controller, but I guess it's the only uncomplicated way. It's not the first time I've wished it was possible to pass objects to a controller, and I'm sure it won't be the last. Maybe we'll get the happy news in a dotnet update one day that we can do things like this more easily.
Related
I have an a href link to a page which adds a parameter to the link for example:
tsw/register-your-interest?Course=979
What I am trying to do is to extract the value in Course i.e 979 and display it in the view. When attempting with the below code, I only return with 0 rather than the course value expected. ideally I'd like to avoid using routes.
Here is the view:
<div class="contact" data-component="components/checkout">
#using (Html.BeginUmbracoForm<CourseEnquiryPageSurfaceController>("PostCourseEnquiryForm", FormMethod.Post, new { id = "checkout__form" }))
{
//#Html.ValidationSummary(false)
#Model.Course;
}
And my controller:
public ActionResult CourseEnquiry(string Course)
{
var model = Mapper.Map<CourseEnquiryVM>(CurrentContent);
model.Course = Request.QueryString["Course"];
return model
}
This is the View Model:
public class CourseEnquiryVM : PageContentVM
{
public List<OfficeLocation> OfficeLocations { get; set; }
public string Test { get; set; }
public string Course { get; set; }
public List<Source> SourceTypes { get; set; }
}
SOLUTION:
After some research and comments I've adjusted the code to the below which now retrieves the value as expected
#Html.HiddenFor(m => m.Course, new { Value = #HttpContext.Current.Request.QueryString["Course"]});
Thanks all
Based on the form code you provided you need to use #Html.HiddenFor(m => m.Course) instead of just #Model.Course. #Model.Course just displays the value as text instead of building a input element that will be sent back to your controller.
If your problem is with a link prior to the view you referenced above, here's what I'd expect to work:
View with link:
#model CourseEnquiryVM
#Html.ActionLink("MyLink","CourseEnquiry","CourseController", new {course = #Model.Course}, null)
CourseController:
public ActionResult CourseEnquiry(string course)
{
// course should have a value at this point
}
In your view, you are only displaying the value of Course.. which isn't able to be submitted. You need to incorporate the value of course with a form input element (textbox, checkbox, textarea, hidden, etc.).
I would highly suggest using EditorFor or Textboxfor, but because your controller action is expecting just a string parameter you could just use Editor or TextBox.
#using (Html.BeginUmbracoForm<CourseEnquiryPageSurfaceController>("PostCourseEnquiryForm", FormMethod.Post, new { id = "checkout__form" }))
{
//#Html.ValidationSummary(false)
#Html.TextBox(Model.Course, null, new { #class = "form-control"});
<input type="submit" value="Submit" />
}
Then you should just be able to do this in your controller:
public ActionResult CourseEnquiry(string course) // parameter variables are camel-case
{
var model = Mapper.Map<CourseEnquiryVM>(CurrentContent);
if(!string.IsNullOrWhiteSpace(course))
model.Course = course;
return model;
}
Let me know if this helps.
This is just something that has been puzzling me, I'm wondering if there's a built in way for this.
Say you have a Package class
public class Package
{
public A AObject { get; set; }
public B BObject { get; set; }
}
And you have a view that uses this Package.
public ActionResult Action()
{
return View(new Package());
}
Now the view will accept this model and have 2 forms.
#model Path.To.Package
#Html.BeginForm("SubmitA", "MyController")
{
#Html.TextBoxFor(m => m.AObject.SomeProperty);
<input type="submit" />
}
#Html.BeginForm("SubmitB", "MyController")
{
#Html.TextBoxFor(m => m.BObject.AnotherProperty);
<input type="submit" />
}
If one would create two actions needed above that take Package as argument, this would work without question...
public JsonResult SubmitA(Package items) { ... }
public JsonResult SubmitB(Package items) { ... }
But at SubmitA the BObject would be null and in SubmitB AObject would be null.
My question here is whether you can submit only a part of the model? So the first form would only submit AObject and the second BObject so you could actually reach these via the following actions:
public JsonResult SubmitA (A a) { ... }
public JsonResult SubmitB (B b) { ... }
You can use the Prefix property of BindAttribute to bind to complex properties of a model. The attribute effectively removes the prefix from the submitted name/value pairs when binding to model.
Your controller methods would be
public JsonResult SubmitA([Bind(Prefix = "AObject")]A model) { ... }
public JsonResult SubmitB([Bind(Prefix = "BObject")]B model) { ... }
You should really use separate view model for each form. You can of course, use bind attribute or use specific property names in the controller action. But, that doesn't solve your real problem. You can only get either of the values and the other object will be unassigned or NULL. This is why you should have separate view model for each view / form. You can build your Package object once you have values for both objects.
I have a CreateViewModel.
public class CreateViewModel
{
public AttributesViewModel AttributesInfo { get; set; }
}
The AttributesViewModel is sent to a partial view.
public class AttributesViewModel
{
public AttributesViewModel()
{
ChosenAttributes = new List<int>();
}
public List<Attributes> Attributes { get; set; }
public List<int> ChosenAttributes { get; set; }
}
The List of Attributes is outputted in the partial view. Each one has a checkbox.
foreach (var attribute in Model.Attributes)
{
<input type="checkbox" name="ChosenAttributes" value="#attribute.ID" /> #Attribute.Name
}
When I post CreateViewModel, AttributesInfo.ChosenAttributes is always empty even though I checked some boxes. How do I properly name each checkbox so that it binds to the ChosenAttributes List?
My Solution
I took Stephen Muecke's suggestion to do the two way binding. So, I created a CheckboxInfo class that contained Value, Text, and IsChecked. I created a EditorTemplate for it:
#model Project.CheckboxInfo
#Html.HiddenFor(model => model.Text)
#Html.HiddenFor(model => model.Value)
#Html.CheckBoxFor(model => model.IsChecked) #Model.Text
One GIANT caveat. To get this to work properly, I had to create an EditorTemplate for the AttributesViewModel class. Without it, when CreateViewModel is posted, it cannot link the checkboxes to AttributesInfo.
Your naming the checkbox name="ChosenAttributes" but CreateViewModel does not contain a property named ChosenAttributes (only one named AttributesInfo). You may be able make this work using
<input type="checkbox" name="AttributesInfo.ChosenAttributes" value="#attribute.ID" /> #Attribute.Name
but the correct approach is to use a proper view model that would contain a boolean property (say) bool IsSelected and use strongly typed helpers to bind to your properties in a for loop or using a custom EditorTemplate so that your controls are correctly names and you get 2-way model binding.
I had a similar scenario, but this was how I did it. The solution is not perfect so please excuse if I have left something out, but you should be able to relate. I tried to simplify your solution as well :)
I changed the Attribute class name to CustomerAttribute, rename it to whatever you like, use a singular name, not plural. Add a property to your CustomerAttribute class, call it whatever you like, I called mine IsChange.
public class CustomerAttribute
{
public bool IsChange { get; set; }
// The rest stays the same as what you have it in your Attributes class
public string Name { get; set; } // I'm assuming you have a name property
}
Delete your AttributesViewModel class, you don't really need it, I like simplicity.
Modify your CreateViewModel class to look like this:
public class CreateViewModel
{
public CreateViewModel()
{
CustomerAttributes = new List<CustomerAttribute>();
}
public List<CustomerAttribute> CustomerAttributes { get; set; }
}
Your controller will look something like this:
public ActionResult Create()
{
CreateViewModel model = new CreateViewModel();
// Populate your customer attributes
return View(model);
}
Your post controller action method would look something like this:
[HttpPost]
public ActionResult Create(CreateViewModel model)
{
// Do whatever you need to do
}
In your view, you will have something like this:
<table>
<tbody>
#for (int i = 0; i < Model.CustomerAttributes.Count(); i++)
{
<tr>
<td>#Html.DisplayFor(x => x.CustomerAttributes[i].Name)</td>
<td>#Html.CheckBoxFor(x => x.CustomerAttributes[i].IsChange)</td>
</tr>
}
<tbody>
</table>
Create a sample app and try out the code above and see if it works for you.
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})
I've the following class structure.
public BaseClass
{
public int Id { get; set; }
public string Name { get; set; }
}
public ClassOne : BaseClass
{
}
public ClassTwo : BaseClass
{
}
My Controller is like this
public TestController : Controller
{
public ActionResult ClassOne()
{
ClassOne model = new ClassOne();
return View("Create",model);
}
public ActionResult ClassTwo()
{
ClassTwo model = new ClassTwo();
return View("Create",model);
}
My View ("Create") is like this :
#model MvcApplication.Models.BaseClass
#using(Html.BeginForm("Post","Test",FormMethod.Post))
{
#Html.HiddenFor(model => mode.Id)
#Html.TextBoxFor(model => model.Name)
<input type="submit" value="Submit"/>
}
My Post Action is the same for both the models i.e. ClassOne & ClassTwo.
On Post how can I know which model is passed in the Post Action whether it is ClassOne or ClassTwo.
[HttpPost]
public ActionResult Post(BaseClass model)
{
/* Code */
}
Inside the post just check the type. Either by if(model is ClassOne) or if(model.GetType() == typeof(ClassOne))
I'm not sure that it is possible without creating your own ModelBinder. In this case you'll always get in your action instance of BaseType class.
You can use typeof():
if(model.GetType() == typeof(ClassOne)){
// do something
}else {
// do something else.
}
Edit:
thanks to #JoreanVannevel
if(model is ClassOne){}
You can just check the type of the object.
if(model.GetType() == typeof(ClassOne))
{
// Do something
}
Jonah,
You should use one model for a view. Now you can do two things
First thing,
you can use custom model binder see here http://www.codeproject.com/Articles/605595/ASP-NET-MVC-Custom-Model-Binder
or (Second thing),
in Html.Beginform also place hidden field to for identifying type (like 1 for ClassOne , 2 for ClassTwo). and now create a third class which has distinct property of both ClassTwo and ClassOne , common property of BaseClass and a extra property for type information.(do not use any type of validation here)
for the post use the newly created type, then check the type field.
your problem is action can't judge which type.because view to action the type is loose.
but we can get type at view and send the type to the action.
view like this:
#model MvcApplication.Models.BaseClass
#using(Html.BeginForm("Post","Test",FormMethod.Post))
{
#Html.HiddenFor(model => mode.Id)
#Html.TextBoxFor(model => model.Name)
#Html.Hidden("type", model.GetType().Tostring())
<input type="submit" value="Submit"/>
}
action like this:
[HttpPost]
public ActionResult Post(BaseClass model)
{
/* Code */
if(Request["type"] == typeof( ClassOne).Tostring())
{
}
if(Request["type"] == typeof( ClassTwo ).Tostring())
{
}
}