Class Libraries, MVC, and Data Annotation - asp.net-mvc

I'm new to MVC and am unsure about proper design.
I have class objects which I use in a variety of applications. I have taken the approach to write a custom view model class so that I can have access to properties in all of these objects and have strong typing. Without re-typing all my class code in the view model is there any way to have the properties in these objects validated using data annotation? Please let me know if my approach and design is all wrong.
[Required]
public User user = new User("username");
//User has lots properites and methods, could i validate inside my class code?
//What I'd like to avoid is putting the following stuff in my custom view model class, //since I already have a class library with this stuff:
public class User
{
[Required]
[StringLength(160)]
public string prop1 { get; set; }
[Required]
[StringLength(160)]
public string prop2 { get; set; }
[Required]
[StringLength(160)]
public string prop3 { get; set; }
public User(string token)
{
SetUser(token);
}
public void SetUser(string token)
{
this.prop1 = "this";
this.prop2 = "this2";
this.prop3 = "this3";
}
============
Good to know I can, but I'm stumbling on some issues. In my view I have: #Html.EditorFor(modelItem => modelItem.user.prop1)
I put the data annotation stuff in my class domain. When it renders it does show the annoations.
<input class="text-box single-line" data-val="true" data-val-length="The field prop1 must be a string with a maximum length of 5." data-val-length-max="5" data-val-required="The prop1 field is required." id="user_prop1" name="user.prop1" type="text" value="somevalue" />
but when I go to my controller the parameter is null. I think because the name is user.prop1. I tried a textbox where i specified the name attribute and yet my controller still couldn't get a value for my parameter.
====================
#model TrainingCalendar.Models.Training
#{
ViewBag.Title = "Signup";
}
<h2>Signup</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm("ConfirmSignup", "Training", FormMethod.Post))
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Training</legend>
<p>
#Html.Label("date", Model.SpecifiedCourse.strClassDate)
</p>
<p>
#Html.Label("time", Model.SpecifiedCourse.Time)
</p>
<p>
#Html.Label("instructor", Model.SpecifiedCourse.Instructor)
</p>
<p>
#Html.Hidden("id", Model.SpecifiedCourse.ID)
</p>
<table>
<tr>
<td>#Html.LabelFor(modelItem => modelItem.ApplicationUser.prop1)</td>
<td>#Html.EditorFor(modelItem => modelItem.ApplicationUser.prop1)</td>
<td style="color:Red">#Html.ValidationMessageFor(modelItem => modelItem.ApplicationUser.prop1)</td>
</tr>
<tr>
<td>#Html.LabelFor(modelItem => modelItem.ApplicationUser.prop2)</td>
<td>#Html.EditorFor(modelItem => modelItem.ApplicationUser.prop2)</td>
<td style="color:Red">#Html.ValidationMessageFor(modelItem => modelItem.ApplicationUser.prop2)</td>
</tr>
</table>
<p>
<input type="submit" value="Sign Up" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
===================
public ActionResult ConfirmSignup(
int id,
string prop1,
string prop2)
{
SignUpForClass();
return View();
}

Absolutely you can have the data annotation attributes on your class code. If you're encapsulating the class in your view model, populated the properties of the encapsulated class in your view. During the validation process the class members will be validated against the data annotation attributes that you specify in the class declaration.

I stumbled along this too and what I ended up doing (I don't know if it is the best or most correct) is too have data annotations that effect primarily the database in the DOM of my class library, such as MaxLength, Required, etc, and then in my view models I have the data annotations that relate primarily to validation such as Regular Expression, Date Time Formats, Max Values, or Max Lengths. In this way I keep the the roles of the two different aspects of the system separate. The View Model is a translation from my DOM to a format that my View can work with and my DOM is specifically for defining what the data should look like in the database.

Related

How to make a CREATE view & controller method for a model that has a list field?

I have these 2 models:
public class Invoice
{
public string InvoiceID {get; set; }
public List<InvoiceElement> InvoiceElements {get; set;}
[...other fields...]
}
public class InvoiceElement
{
public string InvoiceElementID {get; set; }
[ForeignKey("Invoice")]
public string InvoiceID { get; set; }
public virtual Invoice Invoice { get; set; }
public string Item {get; set;}
[...other fields...]
}
I am unable to make a CREATE view for new Invoices that lets me add InvoiceElements.
I want to have a "CurrentInvoiceElements" table where to dinamically add rows.
Just trying to making it simple. You can use the name attribute (the attribute that asp.net uses for modal binding) and post a list along with other properties of the class. You can use javaScript to append new elements to your form. Using the above modals you've provided, I have written a simple example using simple jQuery functions.
Razor View:
<button class="btn btn-success" id="add_btn">Add Invoice Element</button>
#using (#Html.BeginForm("SaveInvoice", "Invoice", FormMethod.Post))
{
<!--Other modal attributes inputs goes here -->
<!--You can use normal html helper extensions -->
<table id="element_table">
<thead>
<tr>
<td>Element Id</td>
<td>Item</td>
</tr>
</thead>
<tbody>
<tr>
<td><input name="invoice.InvoiceElements[0].Item" id="InvoiceElements[0].Item" /></td>
<td><input name="invoice.InvoiceElements[0].InvoiceElementID" id="InvoiceElements[0].InvoiceElementID" /></td>
</tr>
</tbody>
</table>
<input type="submit" />
}
JavaScript:
<script type="text/javascript">
$("#add_btn").on('click', function (e) {
var table = $("#element_table");
var idx = $(table).find("tbody>tr").length;
var htmlToAppend = `<tr>
<td><input name="invoice.InvoiceElements[${idx}].Item" id="InvoiceElements[${idx}].Item" /></td>
<td><input name="invoice.InvoiceElements[${idx}].InvoiceElementID" id="InvoiceElements[${idx}].InvoiceElementID" /></td>
</tr>`;
$(table).find("tbody").append(htmlToAppend);
});
</script>
Controller / Action:
[HttpPost]
public ActionResult SaveInvoice(Invoice invoice)
{
/* your logic here */
if(ModelState.IsValid)
_invoiceBusiness.SaveInvoice(invoice);
return View();
}
Please make sure the variable name in the parameter of the action method matches the name used in the name attribute of the input tag. i.e. name = "invoice.***" public ActionResult SaveInvoice(Invoice invoice)
I followed this solution: https://github.com/danludwig/BeginCollectionItem some years ago, and worked fine.
If I'm not mistaken, at the time I managed to do it using only the HTML Helper: HtmlPrefixScopeExtensions. Then just make sure the name you give on your View when you do Html.BeginCollectionItem("name") is exactly the same as your collection on your ViewModel.
That's for binding back to the controller.
With this, you can dynamically add rows using AJAX per example.
I hope it's clear enough. If you don't understand it I may make a Github repository with this.

List Binding with model data

So I have a form that I am trying to submit and I can get either the list or the model to bind, but not both at the same time. I suspect it has to do with the model binder.
HTML
#using (Html.BeginForm("Index", "Home", FormMethod.Post)){
#Html.AntiForgeryToken()
<div class="TransferHeader">
<div>
#Html.LabelFor(model => model.tranRequestedBy)
#Html.EditorFor(model => model.tranRequestedBy, new { #Name = "h.tranRequestedBy" })
</div>
<div>
#Html.LabelFor(model => model.tranNotes)
#Html.EditorFor(model => model.tranNotes, new { #Name = "h.tranNotes" })
</div>
<input name="h.TransfersDetail.Index" id="detIndex" type="hidden" value="c3a3f7dd-41bb-4b95-b2a6-ab5125868adb">
<input name="h.TransfersDetail[c3a3f7dd-41bb-4b95-b2a6-ab5125868adb].detToolCode" id="detToolCode" type="hidden" value="1234">
</div>
}
Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(TransfersHeader h)
{
return View();
}
Model Class:
public virtual ICollection<TransfersDetail> TransfersDetail { get; set; }
public string tranRequestedBy { get; set; }
public string tranNotes { get; set; }
The two bottom inputs were generated from an AJAX call to an add method, what happens is if they are not present the two HTML helper editors will come in the model, but if they do exist only the transfer detail list will appear.
Is there anything I could do to make sure all of the data comes into the model?
Its not clear how you are generating those inputs, but the name attributes are incorrect. You model does not contain a collection property named h, but it does contain one named TransfersDetail, so your inputs need to be
<input name="TransfersDetail.Index" type="hidden" value="c3a3f7dd-41bb-4b95-b2a6-ab5125868adb">
<input name="TransfersDetail[c3a3f7dd-41bb-4b95-b2a6-ab5125868adb].detToolCode" type="hidden" value="1234">
Its also not clear why your adding an id attribute (if you referencing collection items in jQuery, you would be better off using class names and relative selectors), but the id your using does not have an indexer suggesting that your going to be generating duplicate id attributes which is invalid html (and jQuery selectors would not work in any case)

Passing Data across multiples views using mvc 3 and EF

maybe this question is seen repeatedly around here but i was not able to find a answers.
my project is about reservations for hotels. I have a class Reservation witch has a Icollection of ChoosenRooms and a class that represents de User making the reservation, and other stuff like dates and other stuff.
The process is this:
In my first view I get the chosen rooms, dates, etc, then i pass that to my second view where i´m going to get the user info, and then i have a third view where i want to show all the gathered information so the user can finally click a button to save the data.
My problem is that i need to pass the reservation object class across all these views. In my testing i see that primitive types pass just fine BUT The iColletion of ChoosenRooms is lost when i post back from the view to the next controller action.
can someone post some example how to, Posting back from a view to a controller, complex types like ChoosenRooms inside another class Reservations, are not lost in the process?
Or maybe explain why this info is lost?
the code:
public class Reserva
{
public virtual int ID { get; set; }
[NotMapped]
public string[] q { get; set; }
[Display(Name = "Cliente")]
public virtual Utilizador utilizador { get; set; }
[Display(Name = "Quarto")]
public virtual ICollection<Quartos> ChoosenRooms{ get; set; }
[Display(Name = "Serviços Adicionais")]
public virtual ICollection<ReservasItens> itens { get; set; }
The view
#model SolarDeOura.Models.Reserva
#{
ViewBag.Title = "AddReservaUser";
var _reserva = TempData["reserva"] as Reserva;
}
<h2>AddReservaUser</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Reserva</legend>
<div class="editor-label">
#Html.LabelFor(model => model.dtEntrada)
</div>
<div class="editor-field">
#Html.DisplayFor(model => model.dtEntrada)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.dtSaida)
</div>
<div class="editor-field">
#Html.DisplayFor(model => model.dtSaida)
</div>
<div class="editor-label">
#Model.q.Count() Choosen Rooms
</div>
#foreach (var q in Model.ChoosenRooms)
{
<ul>
<li>
#Html.DisplayFor(modelitem => q.descricao)
</li>
</ul>
}
posting back from here is the Problem. In this view " #foreach (var q in Model.ChoosenRooms)" has data but posting back the data is lost.
The concept of model binder at this point was not very clear to me and some knowledge about this topic is all it takes to solve the question.
In resume:
The view gets a model which is a complex type: class [reserva] has a collection of [ChoosenRooms] which is also a complex type.
The line #Html.DisplayFor(modelitem => q.descricao) renders the necessary html elements to display the data, but not the necessary html to be posted back to the controller (input element or hidden field ) so the model binder fails.
Also the controller (post) action argument didn't had the property name that matches the field, in this case it needed to be a String[] type since its a collection of values.
I would also recommend reading about Display Templates and Editor Templates.

Not able to bind html.checkbox on form post

So I have a view model call ProductViewModel which has a list of sites where a product can be produced. I am trying to figure out how to show this on a form using a checkbox for the user to select the sites where the product can be produced. Seems straight forward, right? Well it doesn't work like I want/need. Hoping someone can help guide me to the correct way to do this.
My classes:
public class ProductViewModel
{
public List<Sites> SiteList {get; set;}
public string ProductName {get; set;}
public int ProductId {get; set;}
public User ProductOwner{get; set;}
}
public class Sites
{
public int SiteId {get; set;}
public string SiteName {get; set;}
public bool IsSelected {get; set;}
}
Part of my view:
#Html.LabelFor(m=>m.Sites):
#foreach (var site in Model.Sites)
{
#Html.CheckBox("Sites", site.IsSelected, new { value = site.SiteName })
#Html.Label(site.SiteName)
}
When using #Html.Checkbox() I see the following output in the html from the browser:
<input checked="checked" id="Sites" name="Sites" type="checkbox" value="Miami" />
<input name="Sites" type="hidden" value="false" />
I understand the hidden field but what I really need is to get the value for the selected item. So I need to get back the list with Miami in it. I don't need the false/true thing that the html helper seem to want to send (i.e. Miami=true)
So instead I tried this.
#for(int id=0; id < Model.Sites.Count(); id++)
{
<input type="checkbox" id="#Model.Sites[id].SiteName" name="Sites[#id]" value="#Model.BoxingSites[id].SiteName" #(Model.Sites[id].IsSelected ? #"checked=""checked""": "") />
#Html.Label(Model.Sites[id].SiteName)
}
And the output is:
<input type="checkbox" id="Miami" name="Sites[0]" value="Miami" checked="checked" />
<label for="Miami">Miami</label>
In both of these cases I am not able to get the binder to map the form values to the Product.Sites list when posting to the action.
The action is like this:
[HttpPost]
public ActionResult Edit(ProductViewModel Product)
{
//Does something with the form data.
}
The other values (ProductName etc...) map fine.
What am I doing wrong? I feel I am missing something as this should be easier due to how MVC simplifies so many other form handling situations.
Thanks in advance...
How about using an editor template instead of struggling with loops:
#model ProductViewModel
#using (Html.BeginForm())
{
... some other form fields
#Html.LabelFor(x => x.SiteList)
#Html.EditorFor(x => x.SiteList)
<input type="submit" value="Create" />
}
and inside the corresponding editor template ~/Views/Shared/EditorTemplates/Sites.cshtml:
#model Sites
<div>
#Html.HiddenFor(x => x.SiteId)
#Html.CheckBoxFor(x => x.IsSelected)
#Html.LabelFor(x => x.SiteName)
</div>
Now not only that your view code is much more clean but proper names will be generated for the input fields so that the model binder will be able to bind the selected values back in the POST action.
[HttpPost]
public ActionResult Create(ProductViewModel model)
{
...
}
Here is what is working for me.
// View Model
[Display(Name="Boolean Property")]
[UIHint("booleancheckbox"]
public bool? booleanProperty;
View
// View
#Html.EditorFor(m => m.booleanProperty, new { #onclick = "Toggle(this);" })
Editor Template - add some more code to handle null values
// Editor Template booleancheckbox.cshtml
#model bool?
#{
labelText = ViewData.ModelMetadata.DisplayName != null ?
ViewData.ModelMetadata.DisplayName :
ViewData.ModelMetadata.PropertyName;
}
<label for="#ViewData.ModelMetadata.PropertyName">#labelText
#Html.CheckBox(string.Empty, Model.Value, ViewContext.ViewData)
</label>

Asp.net MVC2 ModelBindingContext.ModelName empty

I'm not even quite sure where to start explaining this problem. I've been working on this for about the past 10 hours without a clue as to what the root cause is. If any additional details are needed, I'd be happy to provide. I'm just guessing at what is relevant at this point.
I have an MVC2 site with routes set up by by Steve Hodgkiss' wonderful RestfulRouting package, using the default route setup with nested controllers (e.g. /customer/{custid}/location/{locid} and such).
In this, I have one particular model that is giving me issues. For some reason, when the create page post's the data back to my server, the ModelName property in the ModelBindingContext object passed to the DefaultModelBinder (well, my custom class inherited from DefaultModelBinder, to handle grabbing objects from a repository). This happens only for this one model. And I can't spot the differences at all.
The broken model
public class RemedialItem : Entity
{
public virtual int Id { get; set; }
....
A working model:
public class Customer : Entity
{
public virtual int Id { get; set; }
....
Entity is just an empty class used as a marker for Reflection use.
The broken controller method in RemedialItemController.cs
[HttpGet]
public ActionResult New(int? locationId, int? applianceId)
{
var model = ViewModelFactory.Create<CreateRemedialItemViewModel>();
model.Categories = (from c in repository.Query<RemedialItemCategory>()
orderby c.Name
select c).ToList();
model.RemedialItem = new RemedialItem();
return View(model);
}
A working controller method in CustomerController.cs
[HttpGet]
public ActionResult New()
{
var viewModel = ViewModelFactory.Create<SingleCustomerViewModel>();
viewModel.Customer = new Customer();
return View(viewModel);
}
ViewModelFactory is an injected class that handles setting up some basic properties common to all view models (mainly is the user logged in and user details right now)
A broken viewmodel:
public class CreateRemedialItemViewModel : ViewModelBase
{
public RemedialItem RemedialItem { get; set; }
public IList<Location> Locations { get; set; }
public IList<Appliance> Appliances { get; set; }
public IList<RemedialItemCategory> Categories { get; set; }
}
A working ViewModel:
public class SingleCustomerViewModel : ViewModelBase
{
public Customer Customer { get; set; }
}
ViewModelBase contains a handful of properties populated by the ViewModelFactory.
The broken form in thew New view for RemedialItem:
<% using(Html.BeginForm("Create","RemedialItem",FormMethod.Post))
{%>
<%: Html.AntiForgeryToken() %>
<fieldset>
<legend>General</legend>
<div>
<%: Html.LabelFor(m=>m.RemedialItem.Category) %>
<%:Html.DropDownListFor(m=>m.RemedialItem.Category.Id, new SelectList(Model.Categories,"Id","Name")) %>
</div>
<div>
<%: Html.LabelFor(m=>m.RemedialItem.Item) %>
<%: Html.TextAreaFor(m=>m.RemedialItem.Item) %>
</div>
<div>
<%: Html.LabelFor(m=>m.RemedialItem.Note) %>
<%: Html.TextAreaFor(m=>m.RemedialItem.Note) %>
</div>
<input type="submit" value="Create Item" />
</fieldset>
<%}%>
A working New view:
<% using (Html.BeginForm("Create","Customer",FormMethod.Post)) {%>
<%: Html.ValidationSummary(true) %>
<%:Html.AntiForgeryToken() %>
<fieldset>
<legend>Fields</legend>
<p>
<%: Html.LabelFor(m=>m.Customer.Name) %>
<%: Html.TextBoxFor(m=>m.Customer.Name) %>
</p>
<p>
<%: Html.LabelFor(m=>m.Customer.Street) %>
<%: Html.TextBoxFor(m=>m.Customer.Street) %>
</p>
[...tl;dr...]
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
<% } %>
Both produce similar field names:
Broken:
<label for="RemedialItem_Item">Item</label>
<textarea cols="20" id="RemedialItem_Item" name="RemedialItem.Item" rows="2">
</textarea>
Working:
<label for="Customer_Name">Name</label>
<input id="Customer_Name" name="Customer.Name" type="text" value="" />
I apologize for the overly long code dump, in short:
The working set of stuff, when posted back on the create form, has the ModelName set to Customer. The broken stuff is an empty string
Is there something I'm missing? Has anyone encountered something like this before?
I found the issue. In the ViewModel the property that held the instance of RemedialItem to display was called RemedialItem. In the action it posted to, the parameter that took the RemedialItem instance was called item, and that broke everything.
In short, when using ViewModels, make sure the parameter name that takes an object from the ViewModel is the same as the property name in the viewmodel.
There went my day.

Resources