This is a very basic model/view/controller and the model is not coming from the view with any data to the controller. It is probably something basic that I am missing!
Two Models:
Namespace Models
Public Class Search
Public Property Search() As String = String.Empty
Public Property Member() As Member = Nothing
End Class
End Namespace
Namespace Models
Public Class Member
Public Property ContactRefID() As String = String.Empty
End Class
End Namespace
Controller:
Imports Test.Models
Imports Test.Services
Namespace Test
Public Class HomeController
Inherits System.Web.Mvc.Controller
Function Index() As ActionResult
Dim search As New Search
Return View("Index", search)
End Function
<HttpPost()>
Function Index(ByVal search As Search) As ActionResult
Dim member As New Member
Dim homeRepository As New HomeRepository
member = homeRepository.GetMemberData(search)
search.Member = member
Return View("Index", search)
End Function
End Class
End Namespace
View:
#ModelType Test.Models.Search
#Code
ViewData("Title") = "Search Page"
End Code
#Using Html.BeginForm("Index", "Home", FormMethod.Post)
#<div class="row">
<div class="col-md-4">
<label>Search Member/Account Number:</label>
#Html.TextBoxFor(Function(model) model.Search)
<br /><br />
<button id="btnSubmit" type="submit" class="btn btn-primary btn-large clrButton">
Search
</button>
</div>
</div>
<div class="row">
#*...To be filled with Search.Member data*#
</div>
End Using
When I enter a member/account number in the textbox to initiate the search, the search value does not get sent to the 2nd Index function in the controller. It definitely gets called because my breakpoint inside of it gets hit, but the "ByVal search As Search" comes back as Nothing.
I know in 2013 that the get:set items are not needed for the Models, but I tried to add them and it still didn't work.
I also set a value for search.Search in the top index and the textbox gets populated with the value. But when I click the Search button, it doesn't get sent to the 2nd index function either.
Any suggestions?
DefaultModelBinder is unable to bind/recognize a property if the type has the same name - in this case Search. Just change the property to something else and you're good to go...
Generally it's a good practice to think through the objects you're going to create and avoid such tautology. It will be easier for you to maintain these objects if they sounds reasonable. E.g. in this case, you use the name Search for your class. There is no sense to name the sought value Search too. Just name it as it should - SoughtValue
Related
I have two nearly identical scenarios regarding model binding on a controller action. One works, and one doesn't. I can't figure out why.
This works:
Given this ViewModel class:
Public Class SeasonCreateViewModel
Public Property Season As Season
End Class
We have these actions
Function Create() As ActionResult
Dim seasonVM As New SeasonCreateViewModel()
Return View("Create", seasonVM)
End Function
<HttpPost()>
<ValidateAntiForgeryToken()>
Function Create(seasonVM As SeasonCreateViewModel) As ActionResult
End Function
And everything binds perfectly. seasonVM.Season contains the values posted from the form.
HOWEVER, this doesn't work:
Given this ViewModel class:
Public Class UserCreateViewModel
Public UserPerson As UserPersonModel
End Class
And these actions:
Function Create() As ActionResult
Dim userVM As New UserCreateViewModel()
Return View("Create", userVM)
End Function
'
' POST: /Admin/User/Create
<HttpPost()>
<ValidateAntiForgeryToken()>
Function Create(userVM As UserCreateViewModel) As ActionResult
End Function
userVM.UserPerson does not bind to the form values the same way seasonVM.Season does. In fact, it is Nothing (aka. null)
Does anyone have any ideas?
If you're curious about the views, they are structured identically, as in:
#Using Html.BeginForm()
#Html.AntiForgeryToken()
#Html.ValidationSummary(True)
<div class="editor-label">
#Html.LabelFor(Function(model) model.UserPerson.NewUsername)
</div>
<div class="editor-field">
#Html.EditorFor(Function(model) model.UserPerson.NewUsername)
#Html.ValidationMessageFor(Function(model) model.UserPerson.NewUsername)
</div>
<p>
<input type="submit" value="Create" />
</p>
End Using
AND
#Using Html.BeginForm()
#Html.AntiForgeryToken()
#Html.ValidationSummary(True)
<div class="editor-label">
#Html.LabelFor(Function(model) model.Season.SeasonDescription)
</div>
<div class="editor-field">
#Html.EditorFor(Function(model) model.Season.SeasonDescription)
#Html.ValidationMessageFor(Function(model) model.Season.SeasonDescription)
</div>
<p>
<input type="submit" value="Create" />
</p>
End Using
Just a note: I've omitted irrelevant code, mostly just additional properties on the view pages. I will say there is no property named "userVM" on my UserPersonModel as was the case here: Model is null when form submitted
UPDATE
OK. I think I'm about ready to give up on figuring out why Season is binding properly, but UserPerson is not.
I thought I had figured out the answer, but it didn't seem to actually make a difference:
I have
Public Class SeasonCreateViewModel
Public Property Season As Season
End Class
and I have
Public Class UserCreateViewModel
Public UserPerson As UserPersonModel
End Class
When lined up like this, the difference seems obvious. In SeasonCreateViewModel, I have a property Season identically named to the class it is an instance of (Season). In UserCreateViewModel, I have a property UserPerson, which is named slightly differently from its class UserPersonModel. Because of this, I thought the model binder does not automatically match userVM.UserPerson to its corresponding class.
So I changed the class UserPersonModel to UserPerson so the Form values would match up in the same way they do for Season (ie, to the classname), but it STILL did not fix it.
What does fix it, however, is if I change this:
Function Create(userVM As UserCreateViewModel) As ActionResult
to this
Function Create(userPerson As UserPerson) As ActionResult
Why this suddenly binds properly, where it didn't before? I have no idea. Does this help anyone answer this question, though?
As I understand it, you are not creating a new instance of UserPersonModel. I could be very wrong though :)
Public Class UserCreateViewModel
Public UserPerson As UserPersonModel
End Class
I think in your action, you should do something like:
Function Create() As ActionResult
Dim userVM As New UserCreateViewModel()
//userVM.UserPerson = new UserPersonModel() -- in c#
//userVM.UserPerson As New UserPersonModel() -- in VB?
Return View("Create", userVM)
End Function
Or just ignore this answer :P
I've ran into this before and the answer is not at all obvious, but makes sense after you know. If you bind an entire model (rather than a static type), then every single member of that model must be posted in the form, otherwise, the modelbinder doesn't recognize the posted values as an instance of the model.
If you don't want to post every member, but rather just a subset, then you need to create a view model with just the fields you want to post, and use something like AutoMapper or just manually map the fields on the actual model in your POST action.
I'm having issues with my MVC3 vb.net application. When I try to post the changes I've made to the controller, the model is not send to the controller.
I've tried to follow many posts like this one and this one, but I'm not sure of how to implement them in my application since my model did not send IEnumerable types.
At the end I only want that the model returns one value for each batch that is the value that I will save to the database.
When I post the model and try to send to the controller the page sends the following by post:
Client=2&Datacenters=20&BatchesForAssignation[0].CenterID=4&BatchesForAssignation[1].CenterID=20&BatchesForAssignation[1].DatacenterID=14...
But I don't know how to convert this querystring to a BatchesForAssignation object, assign it to the model and send to the controller.
NOTE: The values for Client and Datacenters shown in the querystring are not used in the controller. I need the BatchesForAssignation[n].CenterID part.
Can you please point me to found a solution on this?
This are the objects that I'm using in my MVC application (code compacted):
Batch:
Public class Batch
public property ID as integer
public property CenterID as integer
public property name as string
end class
Centers (This object just store all the list of centers that will be assigned to the Batch. The name is just to show the name in the drop down list):
Public class Center
public property ID as integer
public property name as string
end class
(There's also a Batchlist and a Centerlist objects that acts as collections inherited from CollectionBase that stores all the Batch and Center objects. If you need the class definition please let me know but is pretty strightforward).
The model is as follows
Public class ProcessingModel
public property BatchesForAssignation as BatchList
public property Datacenters as CenterList
End class
The Controller is as follows:
<HttpGet()>
<Authorize()> _
Public Function AssignToDataCenters() As ActionResult
Dim model As New ProcessingModel
Dim BatchHandler As New BatchControl
'This line will get the list of batches without datacenter
model.BatchesForAssignation = BatchHandler.GetBatchesAvailable(ConnectionString)
'This method will get the list of Datacenters available
model.Datacenters=BatchHandler.GetDatacenters(ConnectionString)
return View(model)
End Function
HttpPost (This is actually not working because the model returns an empty model):
<HttpPost()>
<Authorize()> _
Public Function AssignToDataCenters(ByVal Model as ProcessingModel) As ActionResult
Dim BatchHandler As New BatchControl
Dim SaveResult as Boolean=false
'This line will get the list of batches without datacenter
model.BatchesForAssignation = BatchHandler.GetBatchesAvailable(ConnectionString)
'This method save the information returned by the model
SaveResult=BatchHandler.UpdateBatches(model)
ViewBag("Result")=SaveResult
Return View(model)
End Function
The View is as follows (is a Strongly-typed view):
#ModelType MVCAdmin.ProcessingModel
#Code
ViewData("Title") = "Assign Batches To centers"
End Code
#Using Html.BeginForm()
#<table id="tblAvailableBatches">
<thead>
<tr>
<th>Assign batch to:</th>
<th>Name</th>
</tr>
</thead>
<tbody>
#code
For i As Integer = 0 To Model.BatchesForAssignation.Count - 1
Dim a As Integer = i
#<tr>
<td>#Html.DropDownListFor(Function(m) m.BatchesForAssignation(a).CenterID, New SelectList(Model.Datacenters, "ID", "name", model.BatchesForAssignation(i).CenterID), " ")</td>
<td>#Model.BatchesForAssignation(i).name</td>
</tr>
Next
End Code
</tbody>
</table>
<input type="button" value="Apply changes" id="btnApply" />
End Using
Thanks in advance
UPDATE 2012-06-14:
After making some researh I found that I can parse the querystring in the controller using request.Form I can parse the results sent by the view. But the querystring keys are in the form BatchesForAssignation[0].CenterID,BatchesForAssignation[1].CenterID,BatchesForAssignation[2].CenterID and so on...
Is there's a better way to do this "automagically" so that the model parses the querystring and sends the parsed object to the controller?
Again...Thanks in advance
After reviewing this question I've found that the best way to create the model and send it to the controller is creating a CustomModelBinder (from the IModelBinder Interface) and parsing the form's querystring on the BindModel method using the controllerContext.HttpContext.Request.Form property. Something like this:
Public Class ProcessingModelBinder
implements IModelBinder
public Function BindModel(controllerContext As System.Web.Mvc.ControllerContext, bindingContext As System.Web.Mvc.ModelBindingContext) As Object
dim model as ProcessingModel = if(nothing.equals(bindingContext.Model),directcast(bindingContext.Model,directcast(DependencyResolver.Current.GetService(typeof(ProcessingModel))
dim Keys as string()=controllerContext.HttpContext.Request.Form.AllKeys
for each key in Keys
'Fill the model required parameters
Next
return model
End Function
And finally register the new Model Builder in the global.asax file
Sub Application_Start()
AreaRegistration.RegisterAllAreas()
ModelBinders.Binders.Add(typeof(ProcessingModel),New ProcessingModelBinder())
RegisterGlobalFilters(GlobalFilters.Filters)
RegisterRoutes(RouteTable.Routes)
End Sub
I hope that this helps someone
Regards
Edit one more time:
So it looks like I have figured out the address part too, in the class I have:
Public ReadOnly Property addresses As IEnumerable(Of Address)
Get
Return _these_addresses
End Get
End Property
And in the template I have:
<% For Each item In Model.addresses%>
<tr>
<td>
<a href='<%: Url.Action("Edit", "Address", New With { .pid=Model.ContactID, .id=item.AddressID }) %>'>
<img src='<%: Url.Content("~/Content/Images/Edit.jpg") %>' alt="Edit" />
</a>
<a href='<%: Url.Action("Details", "Address", New With { .pid=Model.ContactID, .id=item.AddressID }) %>'>
<img src='<%: Url.Content("~/Content/Images/Detail.jpg") %>' alt="Details" />
</a>
<a href='<%: Url.Action("Delete", "Address", New With { .pid=Model.ContactID, .id=item.AddressID }) %>'>
<img src='<%: Url.Content("~/Content/Images/Delete.jpg") %>' alt="Delete" />
</a>
</td>
<td>
<%: item.Street%>
</td>
<td>
<%: item.City%>
</td>
<td>
<%: item.StateID%>
</td>
<td>
<%: item.CountryID%>
</td>
<td>
<%: item.Zip%>
</td>
</tr>
<% Next%>
</table>
Edit Again:
I added this for every field in contact to the class and it is working...I just need to figure out the list of addresses now...
Public ReadOnly Property FirstName() As String
Get
Return _this_contact.FirstName
End Get
End Property
E D I T:
I about have it figured out:
I took a shot in the dark and made a ContactViewModel based of C examples I have found in research on how to do this
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Web
Imports TotallyAwesomeCRM.Contact
Imports TotallyAwesomeCRM.Address
Public Class ContactViewModel
Public contact As Contact
Public address As Address
Private _this_contact As Contact
Private _these_addresses As System.Linq.IQueryable(Of Address)
Sub New(ByVal this_contact As Contact, ByVal these_addresses As System.Linq.IQueryable(Of Address))
' TODO: Complete member initialization -this was added by the framework for me when I tried to call this class - I don't know what to do here - resources?
_this_contact = this_contact
_these_addresses = these_addresses
End Sub
End Class
So in my controller I:
Function Details(ByVal id As Integer) As ActionResult
Dim this_contact = GetContact(id)
Dim these_addresses =
From address In addressDataContext.Addresses, xref In addressDataContext.ContactAddressesXrefs
Where address.AddressID = xref.AddressID And xref.ContactID = id
Select address
Dim viewModel = New ContactViewModel(this_contact, these_addresses)
Return View(viewModel)
End Function
And in the template it found the contact when I started typing Model.contact
<%: Model.contact.FirstName%>
But it gave me an error there: NullReferenceException: Object reference not set to an instance of an object.
It shouldn't be null...Please help me figure out the TODO
=================================================================================
O R I G I N A L P O S T:
This is my first .NET venture ever. Everything I have done thus far I have figured out.
OK I have contacts, and contacts can have many addresses. I would like to display those addresses when I am viewing the detail of a Contact I would also like to have a container that is a list view of it's addresses. From that list view I want to be able to edit/delete/view the address.
My database structure is:
Tables:
Contacts
<contains contact info and PK>
Addresses
<contains address info and PK>
ContactAddressesXref
ContactID
AddressID
I have basically been editing the skeleton files the ASP.NET MVC empty application provides me
So here is the last thing I have tried in my Contact controller:
'
' GET: /Contacts/Details/5
Function Details(ByVal id As Integer) As ActionResult
Dim this_contact =
From contact In dataContext.Contacts, address In addressDataContext.Addresses, xref In addressDataContext.ContactAddressesXrefs
Where address.AddressID = xref.AddressID And xref.ContactID = id
Select contact, address
Return (View(this_contact))
End Function
What I really wanted to do was query the addresses separately and send them in as their own set. I don't know if doing this is the right thing to do, but when I tried to send in
two models it freaked out.
Which fails of course because the view has this in it:
<%# Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage(Of IEnumerable (Of TotallyAwesomeCRM.Contact))" %>
I tried:
Inherits="System.Web.Mvc.ViewPage(Of IEnumerable (Of TotallyAwesomeCRM.Contact, Of TotallyAwesomeCRM.Address))
It said:
'InitializeCulture' is not a member of 'ASP.views_contacts_details_aspx'.
So OK I tried:
Inherits="System.Web.Mvc.ViewPage"
And it throws and error here:
<div class="display-field"><%: Model.FirstName%></div>
Of course, so Am I wrong in the controller? I know I can't have just System.Web.Mvc.ViewPage(Of IEnumerable (Of TotallyAwesomeCRM.Contact)) that it will have to accept more than that. I tried jacking with the Model.FirstName part saying Contact.FirstName, but that didn't come up in the drop down when I started writing Contact. I could do this easily in other languages, .Net seems to be a different ball game. Please help!
You don't need "todo: complete member initialization..." (besides remove it :-)). You need to change the property for contact and address.
Try this:
Public Class ContactViewModel
Private _this_contact As Contact
Private _these_addresses As System.Linq.IQueryable(Of Address)
Sub New(ByVal this_contact As Contact, ByVal these_addresses As System.Linq.IQueryable(Of Address))
' TODO: Complete member initialization -this was added by the framework for me when I tried to call this class - I don't know what to do here - resources?
_this_contact = this_contact
_these_addresses = these_addresses
End Sub
Public ReadOnly Property Contact As Contact
Get
Return _this_contact
End Get
End Property
Public ReadOnly Property Addresses As System.Linq.IQueryable(Of Address)
Get
Return _these_addresses
End Get
End Property
End Class
I have a pair of views in my application that both display the same Editor Template for one of my model items; of the two views ("Add" and "Edit"), "Edit" works fine, but "Add" is returning null for the model when my controller action handles the post.
I found that if I give the "Add" view a custom ViewModel and call Html.EditorFor(p => p.PageContent) rather than simply calling the EditorFor() on the whole Model object- Html.EditorFor(p => p), then the form returns the correct, non-null model, but that generates other problems pertaining to my client-side scripting and control IDs (as now all of the fields are prefixed with "PageContent_"). I am using the same Editor Template technique in a few different places throughout my application and none of the others are exhibiting this odd dependency on a ViewModel.
Has anyone else ever experienced similar problems?
Edit View
<%# Page Title="" Language="C#" MasterPageFile="~/Areas/Admin/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<PageContent>" %>
<% using (Html.BeginForm())
{ %>
<%=Html.Hidden("PageID", Model.Page.ID) %>
<%=Html.EditorFor(p => p)%>
<input type="submit" name="btnSave" value="Save" />
<input type="submit" name="btnCancel" value="Cancel" class="cancel" />
<% }
Action (Working)
[HttpPost, ValidateInput(false)]
public ActionResult EditContent(int id, FormCollection formCollection) {}
Add View
<%# Page Title="" Language="C#" MasterPageFile="~/Areas/Admin/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<PageContent>" %>
<% using (Html.BeginForm())
{ %>
<%=Html.Hidden("PageID", ViewData["PageID"]) %>
<%=Html.EditorFor(p => p)%>
<input type="submit" name="btnSave" value="Save" />
<input type="submit" name="btnCancel" value="Cancel" class="cancel" />
<% } %>
Action (Failing)
// content is ALWAYS null
[HttpPost, ValidateInput(false)]
public ActionResult AddContent(PageContent content, FormCollection formCollection) {}
Before you cry "duplicate"
This question does relate to this one, but this question is intended to target the specific problem I am experiencing rather than the more general question asked there.
I tracked down the problem and it's a rather interesting one.
When the DefaultModelBinder attempts to resolve a model item one of the first things it does is check to see if there are any prefixed fields in the data being bound; it does this by checking for any form items that begin with the name of the model object (this seems extremely arbitrary, if you ask me). If any "prefixed" fields are found then it results in different binding logic being invoked.
ASP.NET MVC 2 Preview 2 BindModel() Source
public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
if (bindingContext == null) {
throw new ArgumentNullException("bindingContext");
}
bool performedFallback = false;
if (!String.IsNullOrEmpty(bindingContext.ModelName) && !DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, bindingContext.ModelName)) {
// We couldn't find any entry that began with the prefix. If this is the top-level element, fall back
// to the empty prefix.
if (bindingContext.FallbackToEmptyPrefix) {
/* omitted for brevity */
};
performedFallback = true;
}
else {
return null;
}
}
// Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
// or by seeing if a value in the request exactly matches the name of the model we're binding.
// Complex type = everything else.
if (!performedFallback) {
/* omitted for brevity */
}
if (!bindingContext.ModelMetadata.IsComplexType) {
return null;
}
return BindComplexModel(controllerContext, bindingContext);
}
The controller action I defined to handle the Add action defines a PageContent item called "content" and in my domain PageContent has a property called "Content" which "matched" with the model name of "content" thus causing the DefaultModelBinder to assume I had a prefixed value when in fact it was simply a member of PageContent. By changing the signature-
from:
[HttpPost, ValidateInput(false)]
public ActionResult AddContent(PageContent content, FormCollection formCollection) {}
to:
[HttpPost, ValidateInput(false)]
public ActionResult AddContent(PageContent pageContent, FormCollection formCollection) {}
The DefaultModelBinder was once again able to correctly bind to my PageContent model item. I'm not sure why the Edit view didn't also display this behavior, but either way I've tracked down the source of the issue.
It seems to me that this issue falls very close to "bug" status. It makes sense that my view worked initially with the ViewModel because "content" was getting prefixed with "PageContent_", but a core framework feature/bug like this ought not be unaddressed IMHO.
I have a dropdownlist that I populate with some stuff:
In my controller
ViewData["SourceModelList"] = new SelectList(_modelService.GetAllModels(), "Id", "Description");
in my view
<% using (Html.BeginForm("Compare", "Home")) { %>
<p>
<%=Html.DropDownList("SourceModelList")%>
</p>
<p>
<input type="submit" value="Compare" />
</p>
<% } %>
And this renders lovely. Now when I post back to my 'compare' action, how do I find out which item was selected in the drop down?
The name "SourceModelList" should correspond with the name of a field in your ViewModel, so that the binder has something to bind the value of the dropdown to.
Alternatively, you can pluck the value out of the FormCollection object, if your view is not strongly-typed.
The NerdDinner Tutorial goes into this process in greater detail:
NerdDinner Step 5: Create, Update, Delete Form Scenarios
http://nerddinnerbook.s3.amazonaws.com/Part5.htm
You can use any of the regular methods for getting items from a form in ASP.NET MVC: FormCollection, Request object, binding to a specific model or having an action which takes a string SourceModelList parameter.
You can do:
int value = Convert.ToInt32(Request.Form["SourceModelList"]);
Or by ModelBinders just making sure that your model have a property
public int SourceModelList {get; set;}
And the ModelBinder will get it for you.
Or, but less likely:
public ActionResult Name(FormCollection f, int SourceModelList)