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.
Related
I have the following scenario:
public class Stay
{
[Contained]
public Guest PrimaryGuest {get;set;}
}
public abstract class Guest
{
public int ID {get; set;}
}
public class EntityGuest : Guest
{
public string EntityName {get;set;}
}
public class PersonGuest : Guest
{
public string SurName {get;set;}
public string GivenName {get;set;}
}
When querying for the stays, I wish to order by a PersonGuest/SurName.
I know how to order by a child property: [URL]/Stays?$expand=PrimaryGuest&$orderby=PrimaryGuest/ID - but how would I order by on a child property that is derived? Is it even possible? I could not determine it by the OData documentation - it wasn't at least called out for contained entities.
This answer helped me a lot in a similar scenario: oData $expand on Derived Types
Basically you can 'Cast' any complex or entity typed property in your query by adding a forward slash and the qualified name of the model type, using the namespace you have defined for your model, not the .Net full type name.
[URL]/Stays?$expand=PrimaryGuest&$orderby=PrimaryGuest/ModelNamespace.PersonGuest/Surname
If you are unsure of the model namespace, look at the model builder code, or use something similar to this:
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.Namespace = "MyAppModel";
Then your URL should look like this:
[URL]/Stays?$expand=PrimaryGuest&$orderby=PrimaryGuest/MyAppModel.PersonGuest/Surname
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;}
}
I can not figure out what I'm doing wrong here. New to MVC and new to Entity, so I know that's holding me back. Any time I call up AuthUser, AuthRole is always nothing, so I end up doing something like:
authuser.AuthRole = db.AuthRoleSet.Find(2) 'AuthRoleID of 2 = User
This just feels clunky to me. How do I get my property to actually get the role with the user?
Here's my class structure:
Public Class AuthUser
'Class Globals
Dim db As New AuthUserContext
'Properties
Public Property AuthUserID() As Integer
<Required()> _
<Display(Name:="User Name")> _
<DomainUserValidation()> _
Public Property UserName() As String
<Display(Name:="Current Role")> _
Public Property AuthRole As AuthRole
End Class
Public Class AuthRole
Public Property AuthRoleID() As Integer
<Required()> _
<Display(Name:="Role Name")> _
Public Property RoleName() As String
<Required()> _
<Display(Name:="Is Administrator")> _
Public Property isAdministrator() As Boolean
<Required()> _
<Display(Name:="Is Active")> _
Public Property isActive() As Boolean
<Required()> _
Public Property AuthUser As ICollection(Of AuthUser)
End Class
Public Class AuthUserContext
Inherits DbContext
Public Property AuthUserSet() As DbSet(Of AuthUser)
Public Property AuthRoleSet() As DbSet(Of AuthRole)
End Class
You have 2 options (sorry c# syntax):
1 - Lazy load AuthRole when you need it - for this, your AuthRole property needs to be declared as virtual
public virtual AuthRole {get;set;}
Now, when/if you try to access AuthRole, EF will get it from database.
For this to work you need to have DbContext.Configuration.LazyLoadEnabled = true
Another alternative is to eager load it by using a query like this:
var myUserWithRole = myContext.AuthUsers.Include("AuthRole").FirstOrDefault(x=>x.Id == userId);
This will get the user and the role from the database.
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
I've this controller
public class AdminController : Controller
{
private IAdministratorService _administratorService;
public AdminController(IAdministratorService administratorService)
{
_administratorService = administratorService;
}
}
And I've this:
private ModelStateDictionary _modelState;
public AdministratorService(IRepository repository, ModelStateDictionary modelState)
{
_repository = repository;
_modelState = modelState;
}
I've configured Dependency Injection for the Controllers so it would load properly except for sending the ModelState from the Container. How do you do it?
Here is one way to handle this problem...
Controller...
Public Class AdminController
Inherits System.Web.Mvc.Controller
Private _adminService as IAdminService
Public Sub New(adminService as IAdminService)
_adminService = adminService
'Initialize the services that use validation...
_adminService.Initialize(New ModelStateWrapper(Me.ModelState))
End Sub
...
End Class
Service...
Public Class AdminService
Implements IAdminService
Private _repository As IAdminRepository
Private _dictionary as IValidationDictionary
Public Sub New(repository as IAdminRepository)
_repository = repository
End Sub
Public Sub Initialize(dictionary As IValidationDictionary) Implements IAdminService.Initialize
_dictionary = dictionary
End Sub
...
End Class
Wrapper Interface...
Public Interface IValidationDictionary
ReadOnly Property IsValid() As Boolean
Sub AddError(Key as String, errorMessage as String)
End Interface
Wrapper implementation...
Public Class ModelStateWrapper
Implements IValidationDictionary
Private _modelState as ModelStateDictionary
Public ReadOnly Property IsValid() As Boolean Implements IValidationDictionary.IsValid
Get
Return _modelState.IsValid
End Get
End Property
Public Sub New(modelState as ModelStateDictionary)
_modelState = modelState
End Sub
Public Sub AddError(key as string, errorMessage as string) Implements IValidationDictionary.AddError
_modelState.AddModelError(key, errorMessage)
End Class
The use of the ModelStateWrapper allows the service classes to be loosely coupled with MVC. Although, we do have a tight coupling between the AdminController and the ModelStateWrapper because of the New statement, but I don't really care because the model state is MVC specific anyway. By doing this, you would not need to register ModelStateWrapper or ModelState with StructureMap.
In your unit tests, you could call the Initialize method on the service after creating the controller to pass in your testing model state and check the validation errors.
I know you had said you were using a ModelStateWrapper, but just wanted to add a more complete example that might help others...
You should really avoid such circular references. Your service class should not depend on the controller or anything in the System.Web.Mvc assembly whatsoever. It is the role of the controller or some action filter or model binder to manipulate the ModelState according to events happening in the service layer.