I have a ViewModel which contains a list of polymorphic objects.
public class InputParameters
{
public InputParameters()
{
InputPrompts = new List<IInputPrompt>();
}
public string Name { get; set; }
public string Path { get; set; }
public IList<IInputPrompt> InputPrompts { get; set; }
}
Which in turn look this:
public interface IInputPrompt
{
string Name { get; set; }
bool IsHidden { get; set; }
bool IsRequired { get; set; }
dynamic Value { get; set; }
}
public class TextPrompt : IInputPrompt
{
public string Name { get; set; }
public bool IsHidden { get; set; }
public bool IsRequired { get; set; }
public dynamic Value { get; set; }
}
public class MultiSelectPrompt : IInputPrompt
{
public string Name { get; set; }
public bool IsHidden { get; set; }
public bool IsRequired { get; set; }
public dynamic Value { get; set; }
public MultiSelectList Values
{
get
{
return new MultiSelectList(((IDictionary<int, string>)Value).ToList(), "Key", "Value");
}
}
}
There is a Editor View Template for each of the derived types, the views look this this:
#model OptionListModelBinding.Models.InputParameters
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
#using (Html.BeginForm())
{
<fieldset>
<legend><strong>#Model.Name</strong></legend>
<div><em>#Model.Path</em></div>
<div>#Html.EditorFor(p => p.InputPrompts)</div>
<div><input type="submit" /></div>
</fieldset>
}
// editor template
#model OptionListModelBinding.Models.InputParameters
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
#using (Html.BeginForm())
{
<fieldset>
<legend><strong>#Model.Name</strong></legend>
<div><em>#Model.Path</em></div>
<div>#Html.EditorFor(p => p.InputPrompts)</div>
<div><input type="submit" /></div>
</fieldset>
}
// editor template
#model OptionListModelBinding.Models.MultiSelectPrompt
#{
ViewBag.Title = "MultiSelectPrompt";
}
<h2>MultiSelectPrompt</h2>
<div><strong>#Model.Name</strong></div>
<div><em>#Html.ListBox(Model.Name, Model.Values)</em></div>
This all renders nicely:
The question is this:
How do I bind this back to the model? I have a custom model binder: (excuse the hacked up nature of this code).
public class InputParameterModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
// iterate over the form fields
foreach (string item in controllerContext.HttpContext.Request.Form.AllKeys)
{
// this is where the problem is ... I cannot use the generic here.
var dog = FromPostedData<string>(bindingContext, item);
}
InputParameters userInput = new InputParameters();
return userInput;
}
// Dino Esposito code
private T FromPostedData<T>(ModelBindingContext context, string id)
{
if (String.IsNullOrEmpty(id)) return default(T);
var key = String.Format("{0}.{1}", context.ModelName, id);
var result = context.ValueProvider.GetValue(key);
if (result == null && context.FallbackToEmptyPrefix)
{
result = context.ValueProvider.GetValue(id);
if (result == null) return default(T);
}
context.ModelState.SetModelValue(id, result);
T valueToReturn = default(T);
try
{
valueToReturn = (T)result.ConvertTo(typeof(T));
}
catch { }
return valueToReturn;
}
}
EDIT
Did i mention that the items in the list are determined at runtime?
EDIT
This is the front end of a report generating tool. The backend service provides a list of available reports and the parameters needed to run each one. None of this is known at compile time and the report definitions can even change with the web portal needing to be recompiled.
I can have a variable number and type of input parameters.
I don't think you need the custom binder. You can simply pass back your view model to the post method:
[HttpPost]
public ActionResult ProcessForm(InputParameters viewModel)
{
...
}
The default MVC binder will match up the form values with the properties of the InputParameters argument. You may need to use a [Bind] attribute on that argument to indicate a variable name prefix that the binder must look for if your view uses any kind of special form variable names.
You cannot expect MVC to instantiate an IInputPrompt because it has not logic to figure out what class you actually want. Your options are to either create a ModelBinder for model binding to this specific interface, or changing the interface into a concrete class.
Related
I have a model passed from controller to view in my asp.net mvc5 website. Then I show the dropdownlist using the model and I want to pass an id back when submitting the form. Here is my model :
public class SiteDirectionModel
{
public int id { get; set; }
public string name { get; set; }
}
Then in the model, I use a List<SiteDirectionModel> to which I add new instances of each item I need. I fill up both these lists and then pass my model to the view.
#model List<SiteDirectionModel>
#using (Html.BeginForm("GetSiteRF", "Create", FormMethod.Post))
{
#Html.DropDownListFor(x => x.name,new SelectList(Model.name,"Sites"));
<input type="button" value="Selectionner" class="btn btn-primary"/>
}
Then how to retrieve the ids for each name ? And how to pass it as a parameter to my controller? Such that I would have :
public ActionResult GetSiteRF(int id)
{
int newId = id;
//Call method to searchId ...
return View("CreateADUser");
}
I have given how to bind and get value from dropdown. Please use your own BL in this.
Your model should be like this.
public class Something
{
public int Id { get; set; }
public string Name { get; set; }
}
public class SiteDirectionModel
{
public SelectList MyDropDown { get; set; }
public int SelectedValue { get; set; }
}
You BL should be like this.
public List<Something> GetListofSomething()
{
//your logic.
}
Your Get method should be like this.
public ActionResult MyGetMethod()
{
SiteDirectionModel model = new SiteDirectionModel();
model.MyDropDown = new SelectList(GetListofSomething(), "key_field_name", "value_field_name", "default_value");
}
Then finally HTML
#Html.DropDownListFor(x => x.SelectedValue,Model.MyDropDown)
I have a model that contains a List of Milestone, I want that list to be populated (inside the model) with a given set of Textboxes in the webpage.
public class Project
{
public string ProjectNumber { get; set; }
public IList<Parameter> Parameters;
public IList<Milestone> MilestoneList = new List<Milestone>();
}
And inside my Html.BeginForm("Edit", "Home", FormMethod.Post, new { Project = Model })) I have the following TextBox.
#for (int i = 0; i < Model.MilestoneList.Count; i++)
{
<td style="align-content: center;">#Html.TextBoxFor(model=>model.MilestoneList[i].Value)</td>
}
My problem in my controller below the milestonelist is always null in model Project
[HttpPost]
public ActionResult Edit(Project project)
{
helper.CreateProject(project, System.Web.HttpContext.Current.User.Identity.Name);
return View();
}
So how should I program so the list inside the model will be populated through TextBoxes?
Your using fields in the model, not properties, so the DefaultModelBinder cannot set the value. Change you model to
public class Project
{
public string ProjectNumber { get; set; }
public IList<Parameter> Parameters { get; set; }
public IList<Milestone> MilestoneList { get; set; }
}
and initialize the collection in in the controller.
Hello I have following problem:
I tried to post a Model through a Form to an other controller-action.
But the received Model is only filled with null elements.
ToolController.cs
public class ToolController : Controller
{
public ActionResult Index()
{
var Model = new EditToolModel{ /* Some data */ };
return View(Model);
}
[HttpPost]
public ActionResult EditToolOverview(EditToolModel myModel)
{
return RedirectToAction("Index", "Tool", new { show = "overview" });
}
}
EditToolModel.cs
public class EditToolModel
{
public Tools tool;
public IEnumerable<Tools> tools { get; set; }
public ToolsExtention tool_extention;
public string latest_version { get; set; }
public string latest_version_type { get; set; }
public string latest_devStep { get; set; }
public IEnumerable<ToolVersionsView> versions { get; set; }
public IEnumerable<DevelopmentStep> developmentSteps { get; set; }
}
Index.cshtml
#model EditToolModel
#{
ViewBag.Title = "Index";
Layout = "~/Layout/_Layout.cshtml";
}
#Html.Partial("ToolOverview", this.Model)
ToolOverview.cshtml
#model EditToolModel
#using (Html.BeginForm("EditToolOverview", "Tool", FormMethod.Post))
{
<div class="contend">
#Html.TextBoxFor(Model => Model.tool_extention.a)
#Html.TextBoxFor(Model => Model.tool_extention.b)
<input type="submit" name="tool_submit" value="Submit"/>
</div>
}
You need to have a getter/setter on the tool_extention property in order for the DefaultModelBinder to work
public ToolsExtention tool_extention { get; set; }
Ditto for the tool property (but your only rendering controls for the tool_extention property in your view)
ToolsExtention Try changing the EditToolOverview property from EditToolModel to ToolsExtention in the form post method.
[HttpPost]
public ActionResult EditToolOverview(ToolsExtention myModel)
{
//_devProv.changeToolExtention(myModel);
return RedirectToAction("Index", "Tool", new { show = "overview" });
}
I am using MVC4 Application. Below is my model
public class ViewModel
{
public string AssignedUserName { get; set; }
public IList<SelectListItem> PossibleAssignees { get; set; }
[Required]
public string Comment { get; set; }
}
View:
<table style="width: 100%; ">
<tr>
<td>Assigned To:</td>
<td>#Html.DropDownListFor(x => x.AssignedUserName, Model.PossibleAssignees)</td>
<td>#Html.ValidationMessageFor(m => m.AssignedUserName, "Assigned To Required")</td>
<tr></table>
PossibleAssignes is a dropdown, the values should be -> EmptyString,"Mr.Barr".
So if the user selected EmptyString means i need to throw validation like " it is Required" field.
i tried with adding [Required] field validator. it is a collection which having some empty string values as drop down value,
so i am not sure how to use the [Required] field for collection which having empty strings.
i don't want to allow empty strings for dropdown.
how can i validate this ?
don't assign an emtpty string to the values in your list, use it as default value instead and the [Required] will work just fine.
In your view use it as:
#Html.DropDownListFor(x => x.AssignedUserName, Model.PossibleAssignees, String.Empty)
and in your viewmodel:
public class ViewModel
{
[Required]
public string AssignedUserName { get; set; }
public IList<SelectListItem> PossibleAssignees { get; set; }
[Required]
public string Comment { get; set; }
}
EDIT (i'll leave my first answer because it might work for someone else)
You can make a custom validation:
public class ViewModel
{
[Required]
[ValidateAssignedUserName()]
public string AssignedUserName { get; set; }
public IList<SelectListItem> PossibleAssignees { get; set; }
[Required]
public string Comment { get; set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class ValidateAssignedUserName : ValidationAttribute
{
private const string _defaultErrorMessage = "Select a user.";
public ValidateAssignedUserName()
: base(_defaultErrorMessage)
{ }
public override bool IsValid(object value)
{
string user = value as string;
if (user != null && user.Length > 0)
{
return true;
}
return false;
}
}
Alternatively, you could also use a custom model binder .e.g
public class ViewModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext) as ViewModel;
if (model.AssignedUserName == "-1")
bindingContext.ModelState.AddModelError("AssignedUserName", "No assignee selected");
return model;
}
}
Global.asax.cs
protected void Application_Start()
{
// .....
ModelBinders.Binders.Add(typeof(ViewModel), new ViewModelBinder());
// ....
}
Change your view slightly :
<td>#Html.ValidationMessageFor(m => m.AssignedUserName)</td>
Remove the text from the 2nd param for the above line.
Doing the above will result in ModelState.IsValid being set to false. When you render the view, the text "No assignee selected" will appear where the ValidationMessageFor() call is made.
So there are options to achieve what you need.
I have the next view model:
public class PickUpLocationViewModel
{
public DateTime PuDate {get;set}
public IAddressViewModel {get;set;}
}
Depends on implementation of IAddressViewModel I want to use appropriate UIHint("Airport"), UIHint("Seaport") etc.. Is it possible? If yes, how?
You could create an extra property on the IAddressViewModel of TemplateName like so:
public interface IAddressViewModel
{
string TemplateName { get; }
}
So then for each class that implements IAddressViewModel you can define a separate template name like:
public class SeaportAddressViewModel : IAddressViewModel
{
public string TemplateName
{
get
{
return "Seaport";
}
}
}
Then in your view you can use one of the overloads of EditorFor like:
#Html.EditorFor(m => m.Address, Model.Address.TemplateName)
This should cause it to use the editor template called Seaport.cshtml.
Let's suppose that you have the following models:
public class PickUpLocationViewModel
{
public DateTime PuDate { get; set }
public IAddressViewModel Address { get; set; }
}
public class AirportAddressViewModel: IAddressViewModel
{
public string Terminal { get; set; }
}
public class SeaportAddressViewModel: IAddressViewModel
{
public int DockNumber { get; set; }
}
and then a controller action:
public ActionResult Index()
{
var model = new PickUpLocationViewModel
{
Address = new AirportAddressViewModel { Terminal = "North" }
};
return View(model);
}
and a corresponding view:
#model PickUpLocationViewModel
#Html.DisplayFor(x => x.Address)
Now you could define the corresponding display/editor templates:
~/Views/Shared/EditorTemplates/AirportAddressViewModel.cshtml:
#model AirportAddressViewModel
#Html.DisplayFor(x => x.Terminal)
~/Views/Shared/EditorTemplates/SeaportAddressViewModel.cshtml:
#model SeaportAddressViewModel
#Html.DisplayFor(x => x.DockNumber)
Now based on the concrete type, ASP.NET MVC will automatically use the correct template.
And when it comes to binding back you will need a custom model binder. I have illustrated one here: https://stackoverflow.com/a/6485552/29407