In my mvc4 application I need to create a view where customers can choose from a list of services, subscribe (by selecting the yes/no option) and give details of the last service date they had the service done and also provide a proposed date for the future service. It should roughly look like as below
.
I have a services table in the database like Services(Id,Name etc) but don't know how shall I combine the other values which I m showing like yes/no and the two dates in a single viewModel and pass it to view and retrieve all the values on post back. In simple words which fields will my viewmodel have? Any ideas. thanks
It sounds like you're asking for more than just a view model. To expand on shenku's answer, this would be my rough/untested approach in VB. It's no way all-inclusive, but hopefully gives you an idea on how to manipulate data, pass it to a view, and get data back on post-back.
Model/DB objects:
Public Class Service
Public Property ServiceID As Integer
Public Property Name As String
End Class
Public Class CustomerService
Public Property CustomerID As Integer
Public Property ServiceID As Integer
Public Property Selected As Boolean
Public Property LastDate As DateTime
Public Property ProposedDate As DateTime
End Class
ViewModel:
Public Class ViewRow
Public Property ServiceID As Integer
Public Property Name As String
Public Property YesSelected As Boolean
Public Property NoSelected As Boolean
Public Property LastDate As String
Public Property ProposedDate As String
End Class
Public Class ViewModel
Public Property TableHeaders As String() = {"Services","Yes","No","Date of Last Service", "Proposed Date"}
Public Property ServiceDetails As List(Of ViewRow)
End Class
Controller:
Public Class HomeController
Inherits System.Web.Mvc.Controller
' Simulating EntityFramework
Protected db As New MyEntities
Function ServiceList() As ActionResult
Dim thisCustomerID As Integer
' *Set user's customer ID*
' Using a LINQ join to combine with other service information
Dim vm As New ViewModel With {
.ServiceDetails = ( _
From custService In db.CustomerService().ToList()
Join service In db.Service().ToList()
On custService.ServiceID Equals service.ServiceID
Where custService.CustomerID.Equals(thisCustomerID)
Select New ViewRow With {
.ServiceID = service.ServiceID,
.Name = service.Name,
.YesSelected = custService.Selected,
.NoSelected = Not custService.Selected,
.LastDate = custService.LastDate.ToString("MMM yyyy"),
.ProposedDate = custService.ProposedDate.ToString("MMM yyyy")
}).ToList()
}
' Passing to a strongly-typed view of type "ViewModel"
Return View("serviceList",model:=vm)
End Function
' This is where you post back, and data can be bound to type "ViewModel"
<HttpPost()> _
Function ServiceList(data As ViewModel) As ActionResult
' *Model Validation / Insert / Update*
' Refresh the page (if you want)
RedirectToAction("ServiceList","Home")
End Function
End Class
Razor View (serviceList.vbhtml):
#ModelType ViewModel
<div>
<table>
<tr>
#For Each head In Model.TableHeaders
#<th>#(head)</th>
Next
</tr>
#For Each detail In Model.ServiceDetails
#<tr id=#(detail.ServiceID)>
<td>#(detail.Name)</td>
<td>#(If(detail.YesSelected,"X",""))</td>
<td>#(If(detail.NoSelected,"X",""))</td>
<td>#(detail.LastDate)</td>
<td>#(detail.ProposedDate)</td>
</tr>
Next
</table>
</div>
To post-back, you'll have to have javascript grab data entered into any input fields (I didn't include any here), and construct a JSON object--with the appropriate data--that reflects the argument in the Controller's post action. I provided an example with an argument of type ViewModel. This means your JSON fields have to match those defined in the ViewModel model, and their values have to match the respective property's data type. ASP.NET will bind the data on post back. Additionally ViewModel is complex, so you can post a list of ViewRow (for multiple record updates). To bind this, your JSON object needs to have the ServiceDetails property that contains an array of objects that in turn have properties of ServiceID, Name, YesSelected, etc.
A collection of services in your viewmodel should do it, the Selected bool of course would represent the yes/no option and probably be bound to a checkbox.
public class ViewModel
{
public IList<Service> Services {get;set;}
}
public class Service
{
public bool Selected {get;set;}
public DateTime LastDate {get;set;}
public DateTime ProposedDate {get;set;}
}
Related
My view is strongly typed to the following model:
public class CitizenDocument{
public string Name{get;set;}
public SocialStatus SocialStat{get;set;}
public AppType ApplicationType{get;set;}
public int Age{get;set;}
public ReplyMethod PreferredReplyMethod{get;set;}
.......//omitted for brevity
}
Here are the three custom types I used above
public Enum SocialStatus{
Servant=1,Worker,Peasent,Student,Unemployed,Pensioner
}
public Enum AppType{
Appliation=1,Complaint,Proposal,Query
}
public Enum ReplyMethod{
Letter=1,Verbal
}
Of course I'll need dropdownlists to present them in the view. The DropDownListFor<> method needs at least two things- one for storing the selected option value, the other one is the SelectList to populate the dropdownlist from. I don't want to pass the SelectList through ViewBag, which means the only way remaining is passing it inside the model. But this way I'll have to have a separate SelectList for all of the above three properties. Am I right? Or is there any other way that I'm not aware of? I've read lots of SO post regarding this but almost everyone recommends going model way as if there will always be one property of a model.
#{
var dictionary = Enum.GetValues(typeof(SocialStatus)).Cast<SocialStatus>().ToDictionary(item => (int)item, item => item.ToLocalizedString());
var selectList = new SelectList(dictionary, "Key", "Value", Model.SubscriptionTypeId);
}
#Html.DropDownListFor(m => m.SocialStatusId, selectList)
So just store id of selected enum in model.
P.S.: ToLocalizedString() is our custom method for getting the name of enum item
I am trying to list the contries in view. I have created a model called tbl_Countries and the code is below
public class tbl_Countries
{
public int ID { get; set; }
public string Country_Name { get; set; }
}
I have a Controller called Home with the following code. I have created an edmx file for TestDB database
public ActionResult Index()
{
TestDBEntities TestdbContext = new TestDBEntities();
var countries = TestdbContext.tbl_Countries.ToList();
return View(countries);
}
Below is my View code
#model IList
displaying the countries using ul li with foreach
If i run the application am getting this error:
The model item passed into the dictionary is of type 'System.Collections.Generic.List1[TestMVC.tbl_Countries]',
but this dictionary requires a model item of type 'System.Collections.Generic.IList1[TestMVC.Models.tbl_Countries]
I just want to show the list of countries in view and I would like to know
Without creating a model class is it possible to bind grid?
Is it mandatory to specify the model name using #model directive in view?
You got this error 'cause you specified in your view #model List, but pass to it List, try to change it in your view to List
Yes, you can delete #model at all, but in this case your view won't be strongly typed, so you won't be able to use intelli sense
create a list of country type in model
public List< tbl_Countries> country{get;set;}
In index page set the value of this List
public ActionResult Index()
{
TestDBEntities TestdbContext = new TestDBEntities();
tbl_Countries objModel=new tbl_Countries();
objModel.country = TestdbContext.tbl_Countries.ToList();
return View(objModel);
}
According to the error message you are expecting a model of the type List<TestMVC.Models.tbl_Countries> in the view which is different from the List<TestMVC.tbl_Countries> type your action method returns.
To resolve this issue, you could create a list your view expects and map the data you got from Entity Framework to it.
For example:
public ActionResult Index()
{
TestDBEntities TestdbContext = new TestDBEntities();
var countries = new List<TestMVC.Models.tbl_Countries>();
countries = (from country in TestdbContext.tbl_Countries
select new TestMVC.Models.tbl_Countries
{
Country_Name = country.Country_Name
}).toList();
return View(countries);
}
To seperate the logic of the view and data access it is a good practice to have models which are independent from your data models, from the EF models in your example.
I would like to know if there is a way to popuplate a model from dropdownlist selection. For example:
My view is bound to the model Employee and the Employee class has a property 'Department' which is another class with its own properties:
My Employee View Model:
public class Employee
{
public string EmployeeName{get;set;}
public Department EmployeeDepartment{get;set;}
public List<Department> AvailableDepartments {get;set;}
}
Department Model:
public class Department
{
public string Code{get;set}
public string Name{get;set;}
public string Description{get;set;}
}
In my view where I enter the employee details I use a dropdown to let the user choose employee department.
#Html.DropDownListFor(
m => m.EmployeeDepartment,
new SelectList(#Model.AvailableDepartments , "Code", "Name")
)
When I submit the form, I get an instance of 'Employee Class' at the controller but obviously the EmployeeDepartment property will be null.
I am aware that if I add a string property 'EmployeeDepartmentCode' and map it to the dropdownlist, it will work. But is there any way I can populate the Department model property rather than using a string property? - because this view model is also used in a grid where it shows the employee department name.
Well, if your EmployeeDepartment type looks like this:
public class EmployeeDepartment {
public int ID { get; set; }
// + other properties
}
Then you can just change your razor code to:
#Html.DropDownListFor(
m => m.EmployeeDepartment.ID,
new SelectList(#Model.AvailableDepartments , "Code", "Name")
)
And then, when you receive the model back in again, the model will contain a non-null EmployeeDepartment with the ID set.
However, I think you're asking if you can then have the model binder automatically go and fetch the rest of the record from the database and fill in the rest of the data? If so, that's a more complex problem - but you can easily patch that up in the controller.
The essence of my question is how to compose these objects (see below) in a sensible way with MVC3 and Ninject (though I am not sure DI should be playing a role in the solution). I can't disclose the real details of my project but here is an approximation which illustrates the issue/question. Answers in either VB or C# are appreciated!
I have several different products with widely varying properties yet all of them need to be represented in a catalog. Each product class has a corresponding table in my database. A catalog entry has a handful of properties specific to being a catalog entry and consequently have their own table. I have defined an interface for the catalog entries with the intent that calling the DescriptionText property will give me very different results based on the underlying concrete type.
Public Class Clothing
Property Identity as Int64
Property AvailableSizes As List(Of String)
Property AvailableColor As List(Of String)
End Class
Public Class Fasteners
Property Identity as Int64
Property AvailableSizes As List(Of String)
Property AvailableFinishes As List(Of String)
Property IsMetric As Boolean
End Class
Public Interface ICatalogEntry
Property ProductId as Int64
Property PublishedOn As DateTime
Property DescriptionText As String
End Interface
Given that the DescriptionText is a presentation layer concern I don't want to implement the ICatalogEntry interface in my product classes. Instead I want to delegate that to some kind of formatter.
Public Interface ICatalogEntryFormatter
Property DescriptionText As String
End Interface
Public Class ClothingCatalogEntryFormatter
Implements ICatalogEntryFormatter
Property DescriptionText As String
End Class
Public Class FastenerCatalogEntryFormatter
Implements ICatalogEntryFormatter
Property DescriptionText As String
End Class
In a controller somewhere there will be code like this:
Dim entries As List(Of ICatalogEntry)
= catalogService.CurrentCatalog(DateTime.Now)
In a view somewhere there will be code like this:
<ul>
#For Each entry As ICatalogEntry In Model.Catalog
#<li>#entry.DescriptionText</li>
Next
</ul>
So the question is what do the constructors look like? How to set it up so the appropriate objects are instantiated in the right places. Seems like generics or maybe DI can help with this but I seem to be having a mental block. The only idea I've come up with is to add a ProductType property to ICatalogEntry and then implement a factory like this:
Public Class CatalogEntryFactory
Public Function Create(catEntry as ICatalogEntry) As ICatalogEntry
Select Case catEntry.ProductType
Case "Clothing"
Dim clothingProduct = clothingService.Get(catEntry.ProductId)
Dim clothingEntry = New ClothingCatalogEntry(clothingProduct)
Return result
Case "Fastener"
Dim fastenerProduct = fastenerService.Get(catEntry.ProductId)
Dim fastenerEntry = New FastenerCatalogEntry(fastenerProduct)
fastenerEntry.Formatter = New FastenerCatalogEntryFormatter
Return fastenerEntry
...
End Function
End Class
Public ClothingCatalogEntry
Public Sub New (product As ClothingProduct)
Me.Formatter = New ClothingCatalogEntryFormatter(product)
End Sub
Property DescriptionText As String
Get
Return Me.Formatter.DescriptionText
End Get
End Property
End Class
...FastenerCatalogEntry is omitted but you get the idea...
Public Class CatalogService
Public Function CurrentCatalog(currentDate as DateTime)
Dim theCatalog As List(Of ICatalogEntry)
= Me.repository.GetCatalog(currentDate)
Dim theResult As New List(Of ICatalogEntry)
For Each entry As ICataLogEntry In theCatalog
theResult.Add(factory.Create(entry))
Next
Return theResult
End Function
End Class
IMHO, I am not really getting any smells off this code other than having to change the factory for every new product class that comes along. Yet, my gut says that this is the old way of doing things and nowadays DI and/or generics can do this better. Suggestions on how to handle this are much appreciated (as are suggestions on a better title...)
I like to just use the default constructor on models for the view and populate them via Automapper.
I would have a view model like this:
public interface IHasDescription
{
public string DescriptionText { get; set; }
}
public class ViewModelType : IHasDescription
{
[DisplayName("This will be rendered in the view")]
public string SomeText { get; set; }
public string DescriptionText { get; set; }
}
And I have a model from the DAL like this:
public class DALModelType
{
public string SomeText { get; set; }
}
So you have something like this in your controller:
var dalModel = someRepository.GetAll();
var viewModel = Mapper.Map<DALModelType, ViewModelType>(dalModel);
And you have the Automapper setup code in some file. This way you only have the conversion code in one place instead of in multiple methods/controllers. You have a custom resolver which uses dependency injection (instead of () => new CustomResolver()) and this will house your logic for getting the display text.
Mapper.CreateMap<IHasDescription, ViewModelType>()
.ForMember(dest => dest.DescriptionText,
opt => opt.ResolveUsing<CustomResolver>().ConstructedBy(() => new CustomResolver()));
Not sure if this works with your workflow but it should be able to get you what you want.
So making a few small changes I got this to work using the Ninject Factory extension.
Biggest change is that my entities have enough info to display either type (clothes or fasteners in my contrived example) if the item is actually clothes then the fastener specific properties will be null and vice versa.
Public Interface IDescribable
ReadOnly Property DescriptionText As String
End Interface
Public Enum ProductType
CLOTHING
FASTENER
End Enum
Public Interface ICatalogEntry
Inherits IDescribable
ReadOnly Property ProductId As Int64
ReadOnly Property PublishedOn As DateTime
ReadOnly Property ProductType As ProductType
End Interface
Public Class CatalogEntryEntity
Public Property ProductId As Long
Public Property ProductType As ProductType
Public Property PublishedOn As Date
Public Property DescriptionText As String
Public Property Color As String
Public Property Finish As String
Public Property IsMetric As Boolean
End Class
Then with this in place I can define my catalog service as follows:
Public Class CatalogService
Private ReadOnly _factory As ICatalogEntryFactory
Private ReadOnly _repository As CatalogRepository
Public Sub New(entryFactory As ICatalogEntryFactory, repository As CatalogRepository)
Me._factory = entryFactory
Me._repository = repository
End Sub
Public Function CurrentCatalog(currentDate As DateTime) As List(Of ICatalogEntry)
Dim items = Me._repository.GetCatalog()
Return (From item In items Select _factory.Create(item.ProductType.ToString(), item)).ToList()
End Function
End Class
Public Interface ICatalogEntryFactory
Function Create(bindingName As String, entity As CatalogEntryEntity) As ICatalogEntry
End Interface
Ninject will provide the factory (which is awesome!) assuming I setup the bindings like this:
theKernel.Bind(Of ICatalogEntry)().To(Of ClothingCatalogEntry)().Named("CLOTHING")
theKernel.Bind(Of ICatalogEntry)().To(Of FastenerCatalogEntry)().Named("FASTENER")
theKernel.Bind(Of ICatalogEntryFactory)().ToFactory(Function() New UseFirstParameterAsNameInstanceProvider())
I've omitted the FastenerCatalogEntry for brevity; the ClothingCatalogEntry is like this:
Public Class ClothingCatalogEntry
Public Sub New(ByVal entity As CatalogEntryEntity)
...
It was this post that helped me the most to figure this out. I used UseFirstParameterAsNameInstanceProvider exactly as shown there.
I have a view model that is shared by two different pages. The view models are fairly similar with the exception of one property: Address. The view model contains name and location fields. However, the customer view's address label should read: Customer Address and the employee view's address label should read: Employee Address. They will also display different error messages.
Here's a simplified version of what I'm trying to accomplish:
public class BaseLocation
{
[Display(Name="Your Name")]
public string Name {get;set;}
public virtual string Address {get;set;}
}
public class CustomerLocation : BaseLocation
{
[Display(Name="Customer Address")]
public override string Address {get;set;}
}
public class EmployeeLocation : BaseLocation
{
[Display(Name="Employee Address")]
public override string Address {get;set;}
}
Then I created a partial for the base location, like so:
#model BaseLocation
***ASP.NET MVC Helpers here: labels, text, validation, etc.
Finally, in the Customer and Employee pages, I would call the partial and send it the subclassed type.
Customer.cshtml
#model CustomerLocation
#Html.Render("_BaseLocation", Model)
Employee.cshtml
#model EmployeeLocation
#Html.Render("_BaseLocation", Model)
The result is that I would not see the data attributes for the specific type. So for example, in the customer page, I would get a label of "Address" instead of "Customer Address".
I'd rather not create two partials with the same data for each specific type, simply because one property in the shared view model should have a different label and error message. What's the best way to go about this? Thanks.
Because of the way view inheritance works and how the model is defined the parameter passed into something like LabelFor and TextBoxFor uses the model type defined in the class. In your case it's going to always be BaseLocation which is why it's not being overridden.
You don't necessarily have to create partials for your class but you will have to create two views one for customer and one for employee. Since you already have two views specific to each type you will just have to create another location view or merge the baselocation view into it's parent.
Customer.cshtml
#model CustomerLocation
#Html.Render("_CustomerBaseLocation", Model)
Employee.cshtml
#model EmployeeLocation
#Html.Render("_EmployeeBaseLocation", Model)
I definitely understand your issue since you only want to change one view and you could have several of these similar types of situations already with BaseLocation.
You could do something like this...
public static IHtmlString LabelTextFor<TModel, TValue>(this HtmlHelper<TModel> html, object model, Expression<Func<TModel, TValue>> expression)
{
MemberExpression memberExpression = (MemberExpression)expression.Body;
var propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null;
//no property name
if (string.IsNullOrWhiteSpace(propertyName)) return MvcHtmlString.Empty;
//get display text
string resolvedLabelText = null;
var displayattrib = model.GetType().GetProperty(propertyName)
.GetCustomAttributes(true)
.SingleOrDefault(f => f is DisplayAttribute)
as DisplayAttribute;
if (displayattrib != null) {
resolvedLabelText = displayattrib.Name;
}
if (String.IsNullOrEmpty(resolvedLabelText)) {
return MvcHtmlString.Empty;
}
TagBuilder tag = new TagBuilder("label");
tag.Attributes.Add("for", TagBuilder.CreateSanitizedId(html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName("")));
tag.SetInnerText(resolvedLabelText);
return new HtmlString(tag.ToString());
}
Then in your _BaseLocation.cshtml you would make a call like:
#Html.LabelTextFor(Model, m => m.Address)
Writing a custom extension method to do this is about all I can think of