This is probably going to be a whopper...
Ok so I'm building a MVC4 website that has a moderately-frequent theme of being able to edit a parent record as well as add/delete/edit child records on the same page. Using the magic of MVC I am able to define a partial view for the child records like so:
#model NFBC.Models.SubMap
<tr id="#String.Concat("SubMap", ViewBag.Index)">
<td class="mapname">
<input type="hidden" name="submaps[#ViewBag.Index].Id" value="#Model.Id" />
<input type="text" name="submaps[#ViewBag.Index].MapName" value="#Model.MapName" />
</td>
<td class="miles"><input type="text" name="submaps[#ViewBag.Index].Miles" value="#Model.Miles" /></td>
<td class="difficulty">#Html.DropDownList("submaps[" + (string)ViewBag.Index.ToString() + "].DifficultyId", (SelectList)ViewBag.Difficulty(Model.DifficultyId))</td>
<td class="elevation"><input type="text" name="submaps[#ViewBag.Index].Elevation" value="#Model.Elevation" /></td>
<td class="mapfile"><input type="text" name="submaps[#ViewBag.Index].MapFile" value="#Model.MapFile" /></td>
<td class="delete"><img src="~/Images/Error_red_16x16.png" /></td>
</tr>
Then in the parent view I simply call the partial view to render all of its children:
<table id="ChoicesTable">
<thead>
<tr>
<th>Map Name</th>
<th>Miles</th>
<th>Difficulty</th>
<th>Elevation</th>
<th>Map File</th>
<th></th>
</tr>
</thead>
for (int i = 0; i < Model.SubMaps.Count; i++)
{
var map = Model.SubMaps.ElementAt(i);
ViewBag.Index = i;
Html.RenderPartial("_MapChoiceEditRow", map);
}
</table>
I am not able to find any documentation on the "sub entity" name syntax (ie: name="subMaps[#ViewBag.Index].Id"), but it works when binding to a model; all of the children are filled in so long as the indexe values start at 0 and aren't missing any values (ie: 0, 1, 2, 4, 5 will result in binding just 0, 1, and 2). Using the magic of jQuery's Ajax call I am able to dynamically insert and delete rows on the client side.
The problem is that I simply cannot figure out a way to reliably use #Html.EditorFor() with the child entity controls. This would be really nice functionality to have since EditorFor injects all of the unobtrusive jquery validation attributes into the html. Right now I'm basically forced to emulate this behavior by adding my own "data-val='true'" tags (not shown in example, I haven't done it yet) all over the place, which to me seems extremely messy.
So I had the brilliant idea of taking the built-in templates and creating my own templates to inject this stuff (as well as some other things of my own, such as Bootstraps "placeholder" attribute for "helper" text, and maybe tooltips, etc). I downloaded the MVC source and opened up the default editor templates, but instead of seeing markup that renders the unobtrusive values, instead I just get a whole bunch of helper functions that at some point "magically" render the unobtrusive attributes. I cannot figure out how it's done, since the validation stuff is all packed into internal classes that aren't accessible to me.
Am I missing something here or is this just a weakness of MVC that I'm going to have to work around. I'd really love to not need to emulate the unobtrusive validation attribute generation code on my own, but if it's the only solution I suppose I could do it...
Thanks!
I spent the afternoon playing around with the source code some more, and discovered several key helper methods that allowed me to do what I needed to do.
It's not the prettiest thing in the world, but it goes a long way towards automating what I was doing before. My solution was to create a new TextBoxFor helper method called TextBoxForChild:
public static MvcHtmlString TextBoxForChild<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
string parentName,
int index,
IDictionary<string, object> htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var rules = ModelValidatorProviders.Providers.GetValidators(metadata, htmlHelper.ViewContext).SelectMany(v => v.GetClientValidationRules());
if (htmlAttributes == null)
htmlAttributes = new Dictionary<String, object>();
if (!String.IsNullOrWhiteSpace(metadata.Watermark))
htmlAttributes["placeholder"] = metadata.Watermark;
UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(rules, htmlAttributes);
return htmlHelper.TextBox(String.Format("{0}[{1}].{2}", parentName, index, metadata.PropertyName), metadata.Model, htmlAttributes);
}
I get the "ModelMetadata" object, then I generate the "model validator rules" for the model. Then I fill in my custom "placeholder" html attribute with the watermark metadata value, and finally call "UnobtrusiveValidationAttributesGenerator.GetValidationAttributes", which fills my html attributes dictionary with all of the validation attributes.
I then do some custom string formatting to make sure the input name follows the MVC child entity format, and voila, it works!
FYI in case anyone was wondering where that "Watermark" value comes from, it's a value from "DisplayAttribute", called "Prompt":
public class FamilyMember
{
public int ClubNumber { get; set; }
[Display(Name="Name", Prompt="Name")]
[Required]
public string Name { get; set; }
public string Cell { get; set; }
public string Email1 { get; set; }
public string Email2 { get; set; }
public DateTime? BirthDate { get; set; }
}
Lovely!
Related
I am authoring a Page Object for my company that represents a HTML page which contains many tables (and is badly structured). I am only interested in certain tables on this page, and would like to have a single table to reference on my Page Object for simplicity.
Problems
This page is dynamic and loads various amounts of tables.
The "displayed table" of a single workflow is split into 3 tables in HTML.
Table 1 contains the unique identifier.
Table 2 contains buttons I am not concerned with.
Table 3 (wrapped in a div) contains the actual table data I need to retrieve.
Tables are not organized, grouped, or nested in any fashion.
Only organization is the repeating flat structure of the "displayed table". Structure does not change (generated from ASP.Net)
Goals
Have a ControlList that represents each of the "displayed tables".
Stick with ATATA (I have a Selenium solution for this, but the majority of our Page Objects use ATATA and do not want to deviate)
Store the Name of the Workflow as a variable on each of the Table objects (WorkflowName variable)
Here is an abstraction of the HTML code I am working with.
<div>
<table> <!-- Start of Displayed Table. Shows as a single row header -->
<tbody>
<tr>
<td>
<h2 id='WorkflowHeader'> Workflow Identifier </h2>
</td>
</tr>
</tbody>
</table>
<table>
<!-- This table contains buttons that I am not concerned with -->
</table>
<div>
<table> <!-- Start of multi row table that contains data to be retrieved -->
<tr>
<td>Value I want in a table</td>
<td>Value I want in a table</td>
</tr>
</table>
<br /> <!-- End of "Displayed Table" -->
<!-- The above structure repeats for every Workflow type. basic structure below -->
<table></table>
<table></table>
<div>
<table></table>
</div>
<br />
<!-- Basic repeating table structure above -->
</div>
In my ATATA Page Object, I have the following:
using Atata;
using _ = ProjectNameSpace.WorkflowPageObject;
namespace ProjectNameSpace
{
public class WorkflowPageObject : Page<_>
{
public ControlList<WorkflowTable, _> WorkflowTables { get; private set; }
[ControlDefinition("h2[contains(#id, 'WorkflowHeader')]/../../../../following-sibling::div/table", ComponentTypeName = "table")]
public class WorkflowTable: Table<WorkflowRow, _>
{
[FindByXPath("h2[contains(#id, 'WorkflowHeader')]")]
public H2<_> WorkflowName { get; private set; }
}
[ControlDefinition("h2[contains(#id, 'WorkflowHeader')]/../../../../following-sibling::div/table/tbody/tr"), ComponentTypeName = "row")]
public class WorkflowRow: TableRow<_>
{
[FindByColumnHeader(HeaderName1)]
public Content<string, _> TableData1 { get; private set; }
[FindByColumnHeader(HeaderName2)]
public Content<string, _> TableData2 { get; private set; }
[FindByColumnHeader(HeaderName3)]
public Content<string, _> TableData3 { get; private set; }
[FindByColumnHeader(HeaderName4)]
public Content<string, _> TableData4 { get; private set; }
[FindByColumnHeader(HeaderName5)]
public Content<string, _> TableData5 { get; private set; }
[FindByColumnHeader(HeaderName6)]
public Content<string, _> TableData { get; private set; }
}
}
}
When I get to this page and attempt to access any of the TableData, I get the following error:
{"Unable to locate element: By.XPath: (.//h2[contains(#id,
'WorkflowHeader')]/../../../../following-sibling::div/table/tbody/tr)
[1]\r\nContext element:\r\nTag: table\r\nLocation: {X=X,Y=Y}\r\nSize:
{Width=Width, Height=Height}\r\nText: HeaderName1 HeaderName2 HeaderName3
HeaderName4 HeaderName5 HeaderName6\r\nTableData1 TableData2 TableData3
TableData4 TableData5 TableData6"}
I feel like I am not using the ControlDefinitions correctly. My XPath is sound and is returning multiple elements. If I extract the XPath that is being used to find the element and use AtataContext.Current.Driver.FindElementsByXPath(".//h2[contains(#id, 'WorkflowHeader')]/../../../../following-sibling::div/table/tbody/tr")[1] the correct rows are returned.
Note: This code was obfuscated and any misspellings of variables or typos are most likely due to hand typing portions of code in this post. The code builds and runs.
Any help is greatly appreciated.
I assume that you don't need ControlDefinition at WorkflowRow class. Just remove it and try. When you do find rows of table it is already scoped to appropriate <table> element and looks for the children (rows) inside that element, not the whole page.
I can also recommend you to update ControlDefinition of WorkflowTable to the following:
[ControlDefinition("table[.//h2[contains(#id, 'WorkflowHeader')]]", ComponentTypeName = "table")]
This question already has answers here:
Post an HTML Table to ADO.NET DataTable
(2 answers)
Closed 6 years ago.
Controller:
[HttpPost]
public ActionResult GetChecked(FormCollection formCollection)
{
var checked = formCollection["Checked"].Split(',');
var ids = formCollection["Original.ID"].Split(',');
}
View:
#model IEnumerable<Models.Entry> []
<table>
#using (Html.BeginForm("GetChecked", "ControllerName"))
{
#foreach (var item in Model[0])
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Original.ID)
#Html.HiddenFor(modelItem => item.Original.ID)
</td>
<td>
#Html.DisplayFor(modelItem => item.Original.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.New.Name)
</td>
<td>
#Html.CheckBoxFor(modelItem => item.Checked)
</td>
</tr>
}
</table>
//Then there's another table with Model[1]
Model
public class Entry
{
public Entry()
{
Original = new SomeObject();
New = new SomeObject();
Checked = false;
}
public SomeObject Original { get; set; }
public SomeObject New { get; set; }
public bool Checked { get; set; }
}
This works but the ids-array in my controller gets both a true- and a false value for the checked rows. I read that it's because of the FormCollection.
Question: How can I let GetChecked take an IEnumerable<Models.Entry> as a parameter instead? When I tried it it resulted in a null value.
There are a couple of things that you should change:
When rendering controls from a list or array using CheckBoxFor, EditorFor, etc. you should never use foreach - instead, ALWAYS use a for-loop and apply indexing to your collection. The reason is that indexing creates numbered items in your <form> that then no longer conflict with each other, and those numbered items are precisely what you need to successfully process a list of submitted values. See this answer for a simple example: https://stackoverflow.com/a/15375949/1220550
Don't use FormCollection, use a ViewModel class instead. By using FormCollection you are giving up on Databinding / ModelState / ValidationSummary which together are a superb feature of ASP.NET MVC. It is too much to explain it all here, but here is a great link that does exactly that.
It is best to use a fully defined ViewModel class, not only for databinding (see before) but also for consistency and ease-of-use. Having an array of IEnumerable<X> as your #model is at best confusing, and at worst a cause for errors. And what if suddenly you also want to pass an int? Impossible with IEnumerable<X>[], yet a piece of cake with a ViewModel - just add it to the class.
The result is null because you should bind your IEnumerable interface with a model binder. I think you are looking to create a model binding provider, because a provider can look at the type of the property and then create your custom model binder just for that property.
Take a look on this link also http://odetocode.com/blogs/scott/archive/2009/04/27/6-tips-for-asp-net-mvc-model-binding.aspx
If I'm using an editor template for a repeating table row within a Razor view, is there anyway for my to pass a variable from one iteration of the editor template to the next?
For example - my model may have:
CarType
CarTypeDesc
CarModel
My view would have:
<table>
#Html.EditorFor(m => m.CarList)
</table>
Ideally, I would like to have a heading of CarType and a list of CarModels under it, eg:
Type1 - this is the description of type 1
CarModel1
CarModel2
CarModel3
Type1 - this is the description of type 2
CarModel4
CarModel5
Type3 - this is the description of type 3
CarModel6
I can only get that view, if my template knows what the previous CarType was.
Thanks for any ideas,
Mark
I'd recommend changing your view models to represent the concerns of the different views, so your Model would look like:
string CarType { get; set; }
string CarTypeDesc { get; set; }
IEnumerable<CarModel> CarModels { get; set; }
This then removes the need for your view models to be aware of the preceding one and more closely follows the MVVM pattern.
This can be achieved by creating custom templates for these properties:
<table>
#for (int i = 0; i <= Model.Count; i++)
{
Html.EditorFor(m => Model[i].CarType, "CarTypeTemplate");
Html.EditorFor(m => Model[i].CarModel, "CarModelTemplate");
}
</table>
If you have particular data/class types for your CarType or CarModel, you can just create editor templates have the same name as these types, e.g. Views/Shared/EditorTemplates/CarType.cshtml. Razor would use then these templates for rendering your properties. Then you could just keep your original #Html.EditorFor(m => m.CarList) call. Inside these templates you could, for example, create <tr> elements with padding-left: 20px; for the <td> containing the CarModel.
I do not understand why the DefaultModelBinder in MVC3 does not map the Form Post Data to my action method. I have the following ViewModels:
public class DisplayExcelTableViewModel
{
public string UploadedFileName { get; set; }
public List<string> TableHeaders { get; set; }
public List<TableRowViewModel> TableRows { get; set; }
}
public class TableRowViewModel
{
public List<string> TableColumns { get; set; }
}
They are displayed in a (partial) View using DisplayTemplates:
#using (Html.BeginForm("SubmitExcel", "Home", FormMethod.Post))
{
<fieldset>
<table>
<tr>
<th>#Html.DisplayFor(m => m.TableHeaders)</th>//<input id="TableHeaders_0_" name="TableHeaders[0]" type="text" value="Opportunity Id" />
</tr>
<tr>
<td>#Html.DisplayFor(m => m.TableRows)</td>//<input id="TableRows_0__TableColumns_0_" name="TableRows[0].TableColumns[0]" type="text" value="1-H7PKD9" />
</tr>
</table>
<input type="submit" value="Send" />
</fieldset>
}
And the action method looks like this:
public ActionResult SubmitExcel(DisplayExcelTableViewModel excelTable)
To try whether it worked just with one TableRows I tried:
public ActionResult SubmitExcel([Bind(Prefix = "TableRows")] TableRowViewModel TableRows)
to test I also tried to put List<TableRows> and take out the Bind attribute. It does not work.
I got a runtime exception:
"System.MissingMethodException: No parameterless constructor defined for this object."
May you tell me what I am doing wrong?
Thanks Francesco
The problem is that my ViewModels DID NOT have a parameterless constructor, which is what the Default Model Binder looks for(uses .NET’s Activator.CreateInstance() method, which relies on those types having public parameterless constructors).The solutions in this case are two:
1) Add a parameterless constructor to the ViewModel and the other custom classes wrapped inside it.
2) Create a custom model binder that covers also the case of your ViewModel
Thanks
Source: Pro ASP.NET MVC2 Framework (2nd Edition)
Have you checked (for example with Firebug) whether are form values being posted to the server? I'm asking because Html.DisplayFor usually renders display elements, whereas for posting values you usually have to use Html.EditorFor.
I have few elements on my view textboxes , dropdowns etc. All of them have some unique attributes created like that
<%: Html.DropDownListFor(model => model.MyModel.MyType, EnumHelper.GetSelectList< MyType >(),new { #class = "someclass", #someattrt = "someattrt"})%>
I would like to create a read only version of my page by setting another attribute disabled.
Does anybody know how can I do it using variable that can be set globally?
Something like:
If(pageReadOnly){
isReadOnlyAttr = #disabled = "disabled";
}else
{
isReadOnlyAttr =””
}
<%: Html.DropDownListFor(model => model.MyModel.MyType, EnumHelper.GetSelectList< MyType >(),new { #class = "someclass", #someattrt = "someattrt",isReadOnlyAttr})%>
I don’t want to use JavaScript to do that
I have done something similar to what you are after I think - basically I have a couple of different users of the system and one set have read-only privileges on the website. In order to do this I have a variable on each view model:
public bool Readonly { get; set; }
which is set in my model/business logic layer depending on their role privileges.
I then created an extension to the DropDownListFor Html Helper that accepts a boolean value indicating whether the drop-down list should be read only:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web.Mvc;
using System.Web.Mvc.Html;
public static class DropDownListForHelper
{
public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> dropdownItems, bool disabled)
{
object htmlAttributes = null;
if(disabled)
{
htmlAttributes = new {#disabled = "true"};
}
return htmlHelper.DropDownListFor<TModel, TProperty>(expression, dropdownItems, htmlAttributes);
}
}
Note that you can create other instances that take more parameters also.
Than in my view I simply imported the namespace for my html helper extension and then passed in the view model variable readonly to the DropDownListFor Html helper:
<%# Import Namespace="MvcApplication1.Helpers.HtmlHelpers" %>
<%= Html.DropDownListFor(model => model.MyDropDown, Model.MyDropDownSelectList, Model.Readonly)%>
I did the same for TextBoxFor, TextAreaFor and CheckBoxFor and they all seem to work well. Hope this helps.
Rather than disabling the drop down list, why not replace it with the selected option... if you are doing this for a lot of stuff, you should think about having a read-only view and an editable view...
<% if (Model.IsReadOnly) { %>
<%= Model.MyModel.MyType %>
<% } else { %>
<%= Html.DropDownListFor(model => model.MyModel.MyType, EnumHelper.GetSelectList< MyType >(),new { #class = "someclass", someattrt = "someattrt"})%>
<% } %>
And just as an aside, you only need to escape the attribute name with "#" if it is a reserved word, such as "class".
Update
Okay. I do have an answer for you - but on the condition that you read this stuff before you implement it.
MVC is all about separating the concerns. Putting logic in the controller that is specifically a concern of the view is an abuse of MVC. Please don't do it. Anything specific to the view, like HTML, attributes, layout - none of that should ever feature in "controllerville". The controller shouldn't have to change because you want to change something in the view.
It is really important that you understand what MVC is trying to achieve and that the following example breaks the whole pattern and puts view stuff in entirely the wrong place in your application.
The correct fix would be to have a "read" view and an "edit" view - or to put any conditional logic in the view. But here is a way of doing what you want. :(
Add this property to the Model.
public IDictionary<string, object> Attributes { get; set; }
In the controller you can conditionally set the attributes:
model.Attributes = new Dictionary<string, object>();
model.Attributes.Add(#"class", "test");
if (isDisabled) {
model.Attributes.Add("disabled", "true");
}
Use the attributes in your view:
<%= Html.TextBoxFor(model => model.SomeValue, Model.Attributes)%>