I am writing my first MVC application, and struggling with how best to pass data from my controllers to my views. I have a very simple XML document structured like this. (Yes, this is Magic: The Gathering data)
<setlist>
<set>
<name>Alara Reborn</name>
<block>Shards of Alara</block>
<cards>145</cards>
<code>ARB</code>
</set>
<set>
<name>Archenemy</name>
<code>ARC</code>
</set>
</setlist>
(note that some of the nodes like "Block" and "Cards" are optional.)
On my first attempt, I was trying this:
' Load the set info
Dim doc As New System.Xml.XmlDocument
doc = LoadXML("setinfo.xml")
Dim listSet = doc.GetElementsByTagName("set")
ViewData("sets") = listSet
Then in my view, I was attempting to access the XmlNodeList like this:
<%
If ViewData("sets").count > 0 Then
For i = 1 To (ViewData("sets").count - 1)
%>
<tr>
<td><%= ViewData("sets")(i).SelectSingleNode("code").InnerText%></td>
<td><%= ViewData("sets")(i).SelectSingleNode("name").InnerText%></td>
<td><%= ViewData("sets")(i).SelectSingleNode("block").InnerText%></td>
</tr>
<%
Next
End If
%>
But I get an Object Block or With Block error when trying to access SelectSingleNode("block") on the second "set" node, since that node doesn't have a "block" node.
I also have a feeling that the way I'm approaching the view is all wrong. Is there a better way to get this simple XML data into the view so I can work with it?
You should consider creating a Set class (this will be the Model class in MVC) that the Controller loads the XML into. Then this Set class can then handle the absence of a 'block' element.
Binding your view directly to the serialised representation of your data is generally a bad idea. Even though this is just a first application it would be good to follow the 'rules' of the MVC pattern from the start, and you'll learn/understand more along the way!
A better way to pass your XML document to the view would be to create a class that represents your xml document, serialize your document into the type and then pass the instance of the class to the view.
One easy way to serialize your document into a class is to use the XmlSerializer.
Domain Class:
<System.Xml.Serialization.XmlRoot("setlist")> _
Public Class SetList
Inherits List(Of SetItem)
End Class
<System.Xml.Serialization.XmlType("set")> _
Public Class SetItem
<System.Xml.Serialization.XmlElement("name")> _
Public Name As String
<System.Xml.Serialization.XmlElement("block")> _
Public Block As String
<System.Xml.Serialization.XmlElement("cards")> _
Public Cards As String
<System.Xml.Serialization.XmlElement("code")> _
Public Code As String
End Class
Controller:
Public Class SetController
Inherits System.Web.Mvc.Controller
Function Index() As ActionResult
Using reader As System.IO.FileStream = System.IO.File.OpenRead("SetInfo.xml")
Dim xmlSerializer As New System.Xml.Serialization.XmlSerializer(GetType(SetList))
Dim setList As SetList = xmlSerializer.Deserialize(reader)
Return View(setList)
End Using
End Function
End Class
View (note this is a strongly typed view):
<%# Page Language="VB" Inherits="System.Web.Mvc.ViewPage(Of VB.SetList)" %>
<html>
<head>
<title>Test</title>
</head>
<body>
<div>
<table>
<tr>
<th>Code</th>
<th>Name</th>
<th>Block</th>
</tr>
<%For Each setItem In Model%>
<tr>
<td><%=setItem.Code%></td>
<td><%=setItem.Name%></td>
<td><%=setItem.Block%></td>
</tr>
<%Next%>
</table>
</div>
</body>
</html>
Related
Please forgive me if this question is too stupid but I just started learning ASP for few days...
So I decided to make some simple Web application which will dispaly data from database (Postgres).
To connect to DataBase I using NpgsqlConnection class. I saw few tutorials how to connect to DB i.e. here, but almoust everywhere they are using MSSQL and nowhere I cant find solutions for my case.
So I would like to have model which will be contains all fetched data within I will be able to iterate like this:
<% foreach (var item in Model)
{ %>
<tr>
<td><%: item.Title %></td>
<td><%: String.Format("{0:g}", item.ReleaseDate) %></td>
<td><%: item.Genre %></td>
<td><%: item.Rating %></td>
<td><%: String.Format("{0:F}", item.Price) %></td>
</tr>
<% } %>
So How I should do to achieve this?
I thought to create class
public class Person{
int id;
string Name;
string Surname;
...
}
next create generic List of type Person and after fetch data, add all fetched data to my List. And then somehow pass this List as Model.
I think there is some better way to do it right. Any Suggest?
Nope, you're pretty much on track.
The main difference you see from the tutorials is most of them are probably using entity framework to populate the models. Since you're using postgres...i wouldn't really recommend trying to get entity framework to work with it (I heard it's a nightmare). You can use a different Orm if you like, or just do it with your connection command and reader like you're probably used to.
The way I would do it is create a domain model that looks like just like the database model (looks like what you did with Person)
From there you would populate it in a Controller.
public class PersonController : Controller
{
//this method will map to the Person/Index route by default
public ActionResult Index()
{
//use your npgsqlconnection right now to populate whatever object you'd like
List<Person> people = PopulateFromPostgres();
//here were returning the index view with the model being a list of person
return View(people)
}
}
Then in your view (Views/Person/Index.cshtml i believe in this example)
#model List<Person>
<table>
#foreach (var item in Model)
{
<tr>
<td>#item.Title</td>
<td>#String.Format("{0:g}", item.ReleaseDate)</td>
<td>#item.Genre</td>
<td>#item.Rating </td>
<td>string.Format("{0:F}", item.Price)</td>
</tr>
}
</table>
Let me know if there's more specific area you don't understand here.
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
I get the following error when returning a view:
Server Error in '/' Application.
--------------------------------------------------------------------------------
The view 'student' or its master was not found. The following locations were searched:
~/Views/Student/student.aspx
~/Views/Student/student.ascx
~/Views/Shared/student.aspx
~/Views/Shared/student.ascx
Here is my controller action:
[HttpPost]
public ActionResult SubmitStudent()
{
StudentViewModel model = TempData["model"] as StudentResponseViewModel;
ViewData["id"] = model.Id;
ViewData["name"] = model.Name;
string comment = Request["comment"];
var student = student.studentTable.Where(s => s.studentId == model.Id);
return View(student);
}
Here is my View:
<%# Page Language="C#" Inherits="System.Web.Mvc.ViewPage<IEnumerable<string>>" %>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
<title>Student</title>
</head>
<body>
<div>
Student name listed below:
</div>
<table>
<% foreach (var item in Model) { %>
<tr>
<td>
<%= Html.Encode(item)%>
</td>
</tr>
<% } %>
</table>
</body>
</html>
A few things to consider here.
First of all, returning a view after a HTTP POST is really a bad design choiche. You can google about the PRG Pattern and you will find many articles that will explain why you should always redirect to a HTTP GET which will render your view.
Second, I find strange that your code is looking for a view name "student". As per MVC specification, the controller will look for a view named as the action method unless an overload of the View() method which accepts the view name as parameter is called (which is not your case, at least not in the code you posted).
In your example, it seems like it should look for a view named "SubmitStudent". Again, the model type you declare on your view doesn't match the model you're passing to it. It accepts an IEnumerable<string> but you're passing to it an IQueryable<Student> (that's what your student variable contains).
I think you omitted some parts of your code. The parts you posted don't quite match with one another.
In order for your code to work, you're going to need a view called SubmitStudent.aspx inside the Views\Student\ or Views\Shared\ folders.
It also looks odd that your view inherits a list of strings and not a Student object or whatever type of object your query returns. Your view is expecting an enumerable list of string's
This line is also confusing:
var student = student.studentTable.Where(s => s.studentId == model.Id);
Did you mean:
var student = model.studentTable.Where(s => s.studentId == model.Id);
Your view must be in "Views\Student\" - unless you have changed the view engine settings which I imagine you have not.
So I believe your view is not there.
The standard MVC example to draw an item with the appropriate View Template is:
Html.DisplayFor(m => m.Date)
If the Model object has a property named Date of type DateTime, this returns a string with the HTML from the Display/DateTime.ascx template.
Suppose you wanted to do the same thing, but couldn't use the strongly-typed version - you didn't know the Model's type for this View at compile time. You use the older:
Html.Display("Date");
So here's the hard part.
Suppose the Model is IEnumerable. You don't know what those objects are at compile-time, but at run-time they happen to be objects with a Date of type DateTime again, like:
public class ModelClass
{
public DateTime Date { get; set; }
}
Now suppose you want your View to iterate over those objects and render each out. If all you cared about was the value you could do this:
<%
StringBuilder sb = new StringBuilder();
foreach(object obj in (IEnumerable<object>)Model)
{
Type type = obj.GetType();
foreach(PropertyInfo prop in type.GetProperties())
{
// TODO: Draw the appropriate Display PartialView/Template instead
sb.AppendLine(prop.GetValue(obj, null).ToString());
}
}
%>
<%= sb.ToString() %>
I'm obviously taking some shortcuts to keep this example focused.
Here's the point - how do I fulfill that TODO I've written for myself? I don't just want to get the value - I want to get it nicely formatted like Html.Display("Date"). But if I just call Html.Display("Date"), it inspects the Model, which is an IEnumerable, for a property named Date, which it of course does not have. Html.Display doesn't take an object as an argument to use as the Model (like Html.Display(obj, "Date"), and all the classes and methods I can find that lie underneath appear to be internal so I can't tweak and call into them directly.
There must be some simple way to accomplish what I'm trying to do, but I can't seem to find it.
Just to make sure I'm being clear - here's an example of the code of DateTime.ascx:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<System.DateTime>" %>
<%= Model.ToString("MM/dd/yyyy") %>
And so, ideally, the output from this View that can take any Model, but in this case a list of 3 of these ModelClass objects above, would be:
11/10/2001
11/10/2002
11/10/2003
Because the code would find the Display PartialView for DateTime and render it appropriately for each.
So - how do I fulfill the TODO?
Have a look at the template code in this excellent post from Phil Haack. It seems to come close to what you are looking for: http://haacked.com/archive/2010/05/05/asp-net-mvc-tabular-display-template.aspx
I've found one potential solution to this but I'm not in love with it; it requires using several file-based templates, meaning you can't abstract this easily into a code library for use in multiple projects.
The View:
<%
StringBuilder sb = new StringBuilder();
Type itemType = Model.GetType().GetGenericArguments()[0];
sb.AppendLine("<table>");
// Pass in the Model (IEnumerable<object>)'s generic item type as
// the Model for a PartialView that draws the header
sb.Append(Html.Partial("DisplayTableHead", itemType));
foreach(object item in (IEnumerable<object>)Model)
{
sb.Append(Html.Partial("DisplayTableRow", item));
}
sb.AppendLine("</table>");
%>
<%= sb.ToString() %>
Views/Shared/DisplayTableHead.ascx:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Type>" %>
<tr>
<%
foreach (PropertyInfo prop in Model.GetProperties())
{
%>
<th><%= prop.Name %></th>
<%
}
%>
</tr>
Views/Shared/DisplayTableRow.ascx:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<tr>
<%
Type modelType = Model.GetType();
foreach (PropertyInfo modelField in modelType.GetProperties())
{
%>
<td><%= Html.Display(modelField.Name) %></td>
<%
}
%>
</tr>
But I now see the major flaw in this solution, which is that Clicktricity's posted solution acknowledges details in the ModelMetadata - like whether that particular property is set for display, whether it's complex or not, etc.
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