I have a View that gets some bits of data via Action methods that return JSON data.
Depending on the combination of selected options, the user can fill some fields in a page.
What is the best way to pass the data back to a controller, in order to be saved?
The fields that contain data vary on the options selected;
I don't have a ViewModel object with all fields bound to the View.
At the moment I have this:
#Ajax.BeginForm("MyAction", null, new AjaxOptions
{
}, new { #id = "SaveForm" } )
{
.....
#Html.RadioButton("SomeRadioButton", "bla", false, new { #id = "SomeRadioButton" })
.....
#Html.TextArea("SomeTextArea", new { #id = "SomeTextArea" })
.....
Save
}
How do I get all of those control values in the Action?
I can add something like:
public void MyAction(FormCollection form)
{
.........
}
But I don't really like this option.
What's the cleanest way to implement this?
Thanks in advance
You could define a view model:
public class MyViewModel
{
public string SomeRadioButton { get; set; }
public string SomeTextArea { get; set; }
...
}
and then have your controller action take this view model as argument and leave the default model binder do its job:
[HttpPost]
public void MyAction(MyViewModel model)
{
...
}
I would also take advantage of this view model in the view in order to use strongly typed versions of the helpers:
#Ajax.BeginForm("MyAction", null, new AjaxOptions { }, new { #id = "SaveForm" })
{
#Html.RadioButtonFor(x => x.SomeRadioButton)
...
#Html.TextAreaFor(x => x.SomeTextArea)
...
<button type="submit">Save</button>
}
You can (and mostly should) use custom class for this, which will hold all fields. Read further about Model Binding - that's the way to do it with MVC.
Related
I have difficulties understanding the inner workings of the TreeView widget. I am referring to the Kendo code library example, specifically the Ajax loading snippet:
//Ajax binding data
public JsonResult Employees(string id)
{
XElement element = XElement.Load((Server.MapPath("~/App_Data/employees.xml")));
IEnumerable<Employee> result;
if (!string.IsNullOrEmpty(id))
{
//search for id and return it's children
result = FindByID(id, element.Element("Employee")).Element("items").Elements("Employee").Select(e => ToEmployee(e));
}
else
{
//return first level nodes
result = element.Elements("Employee").Select(e => ToEmployee(e)) ;
}
return Json(result, JsonRequestBehavior.AllowGet);
}
//Find the XML element by Id
private XElement FindByID(string id, XElement element)
{...}
//Convert XML element to Object
private Employee ToEmployee(XElement element)
{
return new Employee()
{
id = int.Parse(element.Element("employeeId").Value),
name = element.Element("name").Value,
hasChildren = element.Element("items") != null
};
}
This is the Model used, which corresponds to the actual XML structure:
public class Employee
{
public int id { get; set; }
public string name { get; set; }
public bool hasChildren { get; set; }
public List<Employee> items { get; set; }
}
The View executes the following code:
#(Html.Kendo().TreeView()
.Name("ajaxTree")
.DataTextField("name")
.DataSource(source =>
{
source.Read(read =>
{
read.Action("Employees", "Home");
});
})
)
What bothers me is the fact that the Model needs to be implemented exactly as in this example. Specifically, the "id" and "hasChildren" properties need to be specified exactly in this manner. Modifying, for example, "id" into "Id" would render this example ineffective and the TreeView would not load. Can somebody help me with the following?
How is the binding actually accomplished?
Why must I design my model with lower-case properties? (I know it sound weird, but it conflicts with the rest of my (group) project's formatting...)
Is there a way to bind the Kendo required "id" and "hasChildren" to other properties (same function, different name)?
Not sure if you got an answer for your question.
This link http://demos.kendoui.com/web/treeview/remote-data.html will help you understand how your model data is bound to treeview. Please go through below links.
HierarchicalDataSource - http://docs.kendoui.com/api/framework/hierarchicaldatasource
DataSource - http://docs.kendoui.com/api/framework/datasource
Model - http://docs.kendoui.com/api/framework/model
For question #2, #3
Yes, it is possible to configure your model properties as below:
schema: {
model: {
id: "EmployeeId",
hasChildren: "HasEmployees",
children: "EmployeeArray"
}
}
I'm working on the first MVC3 project at our company, and I've hit a block. No one can seem to figure out what's going on.
I have a complex Model that I'm using on the page:
public class SpaceModels : List<SpaceModel> {
public bool HideValidation { get; set; }
[Required(ErrorMessage=Utilities.EffectiveDate + Utilities.NotBlank)]
public DateTime EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
}
In the Controller, I create a SpaceModels object with blank SpaceModels for when Spaces get combined (this would be the destination Space).
// Need a list of the models for the View.
SpaceModels models = new SpaceModels();
models.EffectiveDate = DateTime.Now.Date;
models.DisplayEffectiveDate = true;
models.Add(new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
return View("CombineSpaces", models);
Then in the View, I am using that SpaceModels object as the Model, and in the form making a TextBox for the Effective Date:
#model Data.SpaceModels
#using (Html.BeginForm("CombineSpaces", "Space")) {
<div class="EditLine">
<span class="EditLabel LongText">
New Space Open Date
</span>
#Html.TextBoxFor(m => m.EffectiveDate, new {
size = "20",
#class = "datecontrol",
// Make this as a nullable DateTime for Display purposes so we don't start the Calendar at 1/1/0000.
#Value = Utilities.ToStringOrDefault(Model.EffectiveDate == DateTime.MinValue ? null : (DateTime?)Model.EffectiveDate, "MM/dd/yyyy", string.Empty)
})
#Html.ValidationMessageFor(m => m.EffectiveDate)
</div>
<hr />
Html.RenderPartial("_SpaceEntry", Model);
}
The Partial View that gets rendered iterates through all SpaceModels, and creates a containing the Edit fields for the individual SpaceModel objects. (I'm using the List to use the same Views for when the Spaces get Subdivided as well.)
Then on the HttpPost, the EffectiveDate is still back at it's DateTime.MinValue default:
[HttpPost]
public ActionResult CombineSpaces(SpaceModels model, long siteID, long storeID, DateTime? effectiveDate) {
// processing code
}
I added that DateTime? effectiveDate parameter to prove that the value when it gets changed does in fact come back. I even tried moving the rendering of the TextBox into the _SpaceEntry Partial View, but nothing worked there either.
I did also try using the #Html.EditorFor(m => m.EffectiveDate) in place of the #Html.TextBoxFor(), but that still returned DateTime.MinValue. (My boss doesn't like giving up the control of rendering using the #Html.EditorForModel by the way.)
There has to be something simple that I'm missing. Please let me know if you need anything else.
Looking at the source code for DefaultModelBinder, specifically BindComplexModel(), if it detects a collection type it will bind the individual elements but will not attempt to bind properties of the list object itself.
What model binding does is attempt to match the names of things or elements in the view to properties in your model or parameters in your action method. You do not have to pass all of those parameters, all you have to do is add them to your view model, then call TryUpdateModel in your action method. I am not sure what you are trying to do with SpaceModel or List but I do not see the need to inherit from the List. Im sure you have a good reason for doing it. Here is how I would do it.
The view model
public class SpacesViewModel
{
public DateTime? EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
public List<SpaceModel> SpaceModels { get; set; }
}
The GET action method
[ActionName("_SpaceEntry")]
public PartialViewResult SpaceEntry()
{
var spaceModels = new List<SpaceModel>();
spaceModels.Add(
new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
var spacesVm = new SpacesViewModel
{
EffectiveDate = DateTime.Now,
DisplayEffectiveDate = true,
SpaceModels = spaceModels
};
return PartialView("_SpaceEntry", spacesVm);
}
The POST action method
[HttpPost]
public ActionResult CombineSpaces()
{
var spacesVm = new SpacesViewModel();
// this forces model binding and calls ModelState.IsValid
// and returns true if the model is Valid
if (TryUpdateModel(spacesVm))
{
// process your data here
}
return RedirectToAction("Index", "Home");
}
And the view
<label>Effective date: </label>
#Html.TextBox("EffectiveDate", Model.EffectiveDate.HasValue ?
Model.EffectiveDate.Value.ToString("MM/dd/yyyy") : string.empty,
new { #class = "datecontrol" })
Sometimes you need to explicitly bind form data using hidden fields such as
#Html.HiddenField("EffectiveDate", Model.EfectiveDate.)
In order to bind the properties of the SpaceModel object you can add individual properties such as SiteID to the view model or add a SpaceModel property for a single SpaceModel. If you want to successfully bind a complex model, add it as a Dictionary populated with key-value pairs rather than a List. You should then add the dictionary to the view model. You can even add a dictionary of dictionaries for hierarchical data.
I hope this helps :)
I am trying to work out the best way of using a viewmodel in the case of creating a new object.
I have a very simple view model that contains a contact object and a select list of companies.
private ICompanyService _Service;
public SelectList ContactCompanyList { get; private set; }
public Contact contact { get; private set; }
public ContactCompanyViewModel(Contact _Contact)
{
_Service = new CompanyService();
contact = _Contact;
ContactCompanyList = GetCompanyList();
}
private SelectList GetCompanyList()
{
IEnumerable<Company> _CompanyList = _Service.GetAll();
return new SelectList(_CompanyList, "id", "name");
}
I then have contact controller that uses this viewmodel and enable me to select a related company for my contact.
[Authorize]
public ActionResult Create()
{
return View(new ContactCompanyViewModel(new Contact()));
}
My issue is with the create method on the controller.
[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Contact _Contact)
{
try
{
_Service.Save(_Contact);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
The problem is that the view returns an empty contact object, but! the company id is populated, this is because the dropdown list explicitly declares its field name.
#Html.DropDownList("parent_company_id",Model.ContactCompanyList)
The standard html form fields pass the objects values back in the format of contact.forename when using the HTML.EditorFor helper...
#Html.EditorFor(model => model.contact.forename)
I can access them if I use a FormCollection as my create action method paremeter and then explicitly search for contact.value but I cannot use a Contact object as a parameter to keep my code nice and clean and not have to build a new contact object each time.
I tried passing the actual view model object back as a parameter but that simply blows up with a constructor error (Which is confusing seeing as the view is bound to the view model not the contact object).
Is there a way that I can define the name of the Html.EditFor field so that the value maps correctly back to the contact object when passed back to the create action method on my controller? Or Have I made some FUBAR mistake somewhere (that is the most likely explanation seeing as this is a learning exercise!).
Your view model seems wrong. View models should not reference any services. View models should not reference any domain models. View models should have parameterless constructors so that they could be used as POST action parameters.
So here's a more realistic view model for your scenario:
public class ContactCompanyViewModel
{
public string SelectedCompanyId { get; set; }
public IEnumerable<SelectListItem> CompanyList { get; set; }
... other properties that the view requires
}
and then you could have a GET action that will prepare and populate this view model:
public ActionResult Create()
{
var model = new ContactCompanyViewModel();
model.CompanyList = _Service.GetAll().ToList().Select(x => new SelectListItem
{
Value = x.id.ToString(),
Text = x.name
});
return View(model);
}
and a POST action:
[HttpPost]
public ActionResult Create(ContactCompanyViewModel model)
{
try
{
// TODO: to avoid this manual mapping you could use a mapper tool
// such as AutoMapper
var contact = new Contact
{
... map the contact domain model properties from the view model
};
_Service.Save(contact);
return RedirectToAction("Index");
}
catch
{
model.CompanyList = _Service.GetAll().ToList().Select(x => new SelectListItem
{
Value = x.id.ToString(),
Text = x.name
});
return View(model);
}
}
and now in your view you work with your view model:
#model ContactCompanyViewModel
#using (Html.BeginForm())
{
#Html.DropDownListFor(x => x.SelectedCompanyId, Model.CompanyList)
... other input fields for other properties
<button type="submit">Create</button>
}
I have the following action method:-
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
Assessment a = elearningrepository.GetAssessment(id);
try
{
if (TryUpdateModel(a))
{
elearningrepository.Save();
return RedirectToAction("Details", new { id = a.AssessmentID });
}
}
//code does here
but I can not write something like if (TryUpdateModel(a, "Assessment", new string { "Date"})) to specify that I only allow the Date property to be updated.
So how I can add a bind list to the above if (TryUpdateModel(a))?
BR
but I can not write something like
if (TryUpdateModel(a, "Assessment", new string { "Date"}))
That's because you should write it like this, since the allowed properties argument represents a string array:
if (TryUpdateModel(a, "Assessment", new[] { "Date" }))
{
}
I would suggest that you stay away from using TryUpdateModel in general.
The repository usually has an update method that sets the entityState to modified before Save() is called, i cannot see that in the code above.
If your goal is to display a record and only allow date to be saved, then create a view for that model, and render fields with:
This sets the model for the view:
#model YourNamespace.Models.Assessment
#Html.DisplayFor(model=>model.propertyToDisplay)
on the items you only want to display, and a
#Html.EditorFor(model=>model.Date)
In your action controller you take the properties you want to bind to as input parameters:
Edited
class Assessment
{
public int Id { get; set; }
public DateTime Date { get; set; }
//Other properties
}
public ActionResult Edit(int Id, DateTime Date)
{
var assessment = elearningrepository.GetAssessment(id);
assessment.Date = Date;
elearningrepository.UpdateAssessment(assessment);
elearningrepository.Save();
//Redirect to action Detail
}
In this case the model binder should just bind to Id, and Date, so even if someone tries to post other values (editing the html form is easy), parameters in ActionResult should be named exactly as in the Model and use that to fetch and update the entity.
You should validate that the user actually can access and edit that id, or as an alternative use MVC Security Codeplex to check that the Id parameter has not been tampered with. it is really easy and convenient to use, but that is another discussion.
As an alternative you can use an attribute like this, described in this blog, but I don't use that myself:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create( [Bind(Include="Id,Date")] Assessment assessment)
i tried this an it works fine
string[] allowedProperties = new[] { "Date" };
try
{
if (TryUpdateModel(a, allowedProperties))
{
(couldn't think of a better title, sorry)
So I've got my layout page, on this page there is a searchbar + options. Choosing whatever, should take you through to the search page, with the results etc. Fairly standard. What I've done to get this working is to create a MasterModel class, with a SearchDataModel class member on it. This SearchDataModel contains the various parameters for the search (search term, what fields to search on etc).
I've then strongly typed my layout page to the MasterModel class, and using a Html.BeginForm... I've constructed the search form for it. However all the checkboxes relating to the fields aren't checked by default, even though the default value for all the fields is true (via a private getter/setter setup).
Yet when I submit the form to the SearchController, all the checkboxes are set to true. So I'm a bit confused as to why it knows they should be true, yet not set the checkboxes to be checked?
Putting breakpoints in key places seems to show that the model isn't insantiated on the get requests, only the post to the Search controller?
I may be going about this all wrong, so if so, pointers as to the right way always appreciated.
public class MasterModel {
public SearchDataModel SearchModel { get; set; }
}
public class SearchDataModel{
private bool _OnTags = true;
private bool _OnManufacturers = true;
private bool _OnCountries = true;
[Display(Name= "Tags")]
public bool OnTags {
get { return _OnTags; }
set { _OnTags = value; }
}
[Display(Name= "Manufacturers")]
public bool OnManufacturers {
get { return _OnManufacturers; }
set { _OnManufacturers = value; }
}
[Display(Name= "Countries")]
public bool OnCountries {
get { return _OnCountries; }
set { _OnCountries = value; }
}
[Required]
[Display(Name="Search Term:")]
public string SearchTerm { get; set; }
}
Then in the _layout page:
#Html.CheckBoxFor(m => m.SearchModel.OnTags, new { #class="ddlCheckbox", #id="inpCheckboxTag" })
#Html.LabelFor(m =>m.SearchModel.OnTags)
Make sure you return a MasterModel with initialized SearchModel from your views:
public ActionResult Index()
{
var model = new MasterModel
{
SearchModel = new SearchDataModel()
};
return View(model);
}
Another possibility to implement this functionality than strongly typing your master layout to a view model is yo use Html.Action as shown by Phil Haack in his blog post.