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")]
Related
I changed the project design and created database view :
ALTER VIEW [dbo].[samplesList]
AS
SELECT DISTINCT
results.machine_id,
sample_id,
program_id,
Machines.Machine_id AS 'ID',
custid,
sys_users.user_full_name AS 'HospitalName',
Programs.name AS 'ProgramName',
machines.Machine_name AS 'MachineName',
samples.name AS 'SampleName'
FROM
results
INNER JOIN
programs ON RESULTS.program_id = Programs.id
INNER JOIN
Machines ON RESULTS.machine_id = Machines.Machine_id
INNER JOIN
sys_users ON RESULTS.custid = sys_users.user_id
INNER JOIN
samples ON RESULTS.sample_id = samples.id
This is the result in the database :
See the screenshot - it shows the correct data sample no 1, sample no 2, sample no 3 and their machines are correct.
But in the controller when I link the view with the controller its not show same result from the database this is the controller code :
public ActionResult Indexs()
{
int UserId = Convert.ToInt32(Session["UserID"]);
var samples = _context.samplesLists
.Where(x=> x.custid == UserId).ToList();
return View(samples);
}
This is the model :
namespace warehouse.Models
{
using System;
using System.Collections.Generic;
public partial class samplesList
{
public Nullable<int> machine_id { get; set; }
public Nullable<int> sample_id { get; set; }
public Nullable<int> program_id { get; set; }
public int ID { get; set; }
public Nullable<int> custid { get; set; }
public string HospitalName { get; set; }
public string ProgramName { get; set; }
public string MachineName { get; set; }
public string SampleName { get; set; }
}
}
And finally the surprise this is the output for same view in the site :
All data appears as "sample no 1":
This is the view markup:
#model IEnumerable<warehouse.Models.samplesList>
#{
ViewBag.Title = "Indexs";
Layout = "~/Views/Shared/_LayoutDashboard.cshtml";
}
<img style="margin-left:250px;" src="~/images/weblogo.png" />
<p style="margin-left:40px;">
#*<h3 style="margin-left:100px; font-family:Andalus;text-underline-position:below">
#Html.Label("Hospital Name :")
#Html.DisplayFor(model => Model.FirstOrDefault().)
</h3>*#
<table class="table table-bordered">
<tr style="background-color:hotpink">
<th>
#Html.DisplayNameFor(model => model.ProgramName)
</th>
<th>
#Html.DisplayNameFor(model => model.SampleName)
</th>
<th>
#Html.DisplayNameFor(model => model.MachineName)
</th>
#*<th></th>
<th></th>
<th></th>*#
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.ProgramName)
</td>
<td>
#Html.DisplayFor(modelItem => item.SampleName)
</td>
<td>
#Html.DisplayFor(modelItem => item.MachineName)
</td>
Please I need your help what happening why all solutions not working and what I need to change ?
EF has its share of troubles with views - because views typically don't have a defined primary key, which is crucial for EF to detect which data has already been loaded (and which hasn't).
What happens here is: since there's no primary key for your view, EF will just use all non-nullable columns from the view as a "substitute" PK.
And when EF reads the data, it will go:
read the row
check the primary key (or the "substitute" PK in this case)
if it has already read a row with that PK - it just duplicates that row that it already has - it will disregard any non-PK columns actually read from the view!
So in your case, once it's read a first row with the "substitute" PK, any further rows with those same values will just get the already read values - no matter what's stored in the database!
SOLUTION: EF needs a proper primary key to uniquely identify each individual row of data.
One solution is to add every single PK of all the underlying tables, and make sure those are marked as "non-nullable" in your model class. Another solution might be to add a custom ROW_NUMBER() OVER (....) column as an "artificial" dummy PK which gives each row a unique value - so that no two rows being read from the view are considered identical.
I solved it in different way as marc_s said there is different activity in database views and primary keys not deal like the tables primary key
What I did :
I created new TABLE (MASTER_RESULTS) and inserted the data I need without duplicate and created the controller and view based on this master table .
In my razor page PageModel I have a bound List<T> property:
[BindProperty]
public List<JobCard> CustomerSpecificJobCards { get; set; }
Here is the JobCard model:
public partial class JobCard
{
public int JobCardId { get; set; }
public string Products { get; set; }
public string CusCode { get; set; }
public string Category { get; set; }
public bool IsAssigned { get; set; }
}
The list of JobCards is populated after the user posts an order number from the page:
CustomerSpecificJobCards = AllJobCards.Where(jc => jc.CusCode == WorkOrderInfo.MarkForCusCode).ToList();
It is then displayed in a form on the razor page via a foreach loop:
#foreach (var card in Model.CustomerSpecificJobCards)
{
<div class="col-1"><input asp-for="#card.IsAssigned" /></div>
<div class="col-9">#card.Products</div>
<div class="col-2">#card.JobCardId</div>
}
Users are shown a list of job cards and a checkbox. Once checked the user submits the selections. When I look at the CustomerSpecificJobCards that are posted, the list is empty. Why? Based on information here, I decided to change the foreach loop to a for loop:
#for (var card = 0; card < Model.CustomerSpecificJobCards.Count; card++)
{
<div class="col-1"><input asp-for="#Model.CustomerSpecificJobCards[card].IsAssigned" /></div>
<div class="col-9">#Model.CustomerSpecificJobCards[card].Products</div>
<div class="col-2">#Model.CustomerSpecificJobCards[card].JobCardId</div>
}
[EDIT] Originally, I thought all the values were returned using the for loop, but it turns out only the .IsAssigned values are returned... Products and JobCardId are empty. I'm using Razor Pages for the first time. What am I doing wrong?
[Followup] After reading Rafalon's answer, I found this explanation of binding a complex collection with checkboxes in either a for or foreach loop. Plus, here is another excellent related link on data binding.
Short answer: check the generated HTML for both for and foreach and you'll see that with foreach, you can not expect the form to return the List correctly (with asp-for helper).
for is needed so you get indexable inputs, looking like:
<input name='CustomerSpecificJobCards[0].IsAssigned'
id='CustomerSpecificJobCards_0__IsAssigned' />
You could still do it with foreach, but without asp-for it's quite tedious, and your model have to meet some requirements (having an index property for example).
Your other problem comes from the fact that #Model.CustomerSpecificJobCards[card].Products is text only.
Therefore you should either replace it with an input, just like IsAssigned, or else you can add a hidden input:
<input type="hidden" asp-for="Model.CustomerSpecificJobCards[card].Products" />
Same goes for JobCardId.
First time posting.
I'm fairly new to programming and I'm trying to create a website to house the web apps I build. I currently have a model, view, and controller to project the contents of my Apps table. These apps are hosted elsewhere, so the tiles that appear in the screenshot below simply link out to other locations. I'd like to include a way to display recently used apps in a drawer, (the code housed in the _Layout).
_Layout Drawer:
I'd like to start by displaying ANY model data in the _Layout drawer. If I simply throw my Apps View code into the _Layout, like so:
#_Layout, located beneath navbar code and above RenderBody()#
#model IEnumerable<Apps>
<div id="footerSlideContainer">
<div id="footerSlideButton"></div>
<div id="footerSlideContent">
<div id="footerSlideText">
<div class="parenttile">
#foreach (var Apps in Model)
{
<a href="http://#Apps.AppLink" target="_blank">
<div class="tile">
<div></div>
<div class="tilemid">
<div></div>
<div>
<img class="tileimage" src="#Apps.AppImage" alt="#Apps.AppName" />
</div>
<div></div>
</div>
<div class="tilebot">
#Apps.AppName
</div>
</div>
</a>
}
</div>
<h3>Recently Used Apps</h3>
<p>This section will store all of your most recently used apps. It stays on the screen until you click the drawer icon.</p>
</div>
</div>
</div>
...I get the following error in Debug on the #foreach line: NullReferenceException: Object reference not set to an instance of an object. .net core. I researched that error here: What is a NullException error and how do I fix it?
I understand the action method comes before the model displays. If I
were to use the above code, I would presume that I need to inject
the model into the action method - but, does the _layout have a
controller?
I researched here: ASP MVC Razor Pass model to view . This
looks like the closest solution, but would appreciate someone
walking me through it.
Finally, I noticed that I could use a ViewComponent ViewComponent
in ASP.NET Core. I could use some help implementing this solution.
Which of these solutions would work/work best?
Then there's the issue of creating the favorites. I think that's a bit more straightforward, as this Display recently viewed items contains all the info I think I need.
Thanks for any help - I'm including my model and controller code below:
Model:
public class Apps
{
public int AppID { get; set; }
public string AppName { get; set; }
public string AdGroup { get; set; }
public string AppDescription { get; set; }
public string AppLink { get; set; }
public string AppImage { get; set; }
}
Controller (using hard coded, dummy data for now, before I move to db)
public class AppsController : Controller
{
private List<Apps> _apps;
public AppsController()
{
_apps = new List<Apps>();
//creating test Apps model
_apps.Add(new Apps
{
AppID = 1,
AppName = "Test App 1",
AdGroup = "Group 1",
AppDescription = "First test app.",
AppLink = "www.google.com",
AppImage = "/images/image1.png"
}); //et al
}
public IActionResult Index()
{
return View(_apps);
}
}
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.
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!