How I can replace an existing html val with a value calculated later using html and/or css? - html

I am creating html to pass back from a REST method, such as:
StringBuilder builder = new StringBuilder();
builder.Append("<div class=\"row\">");
builder.Append("<div class=\"col-md-6\">");
builder.Append("<div class=\"bottomleft\">");
// TODO: Replace the hardcoded val ("$9,814.81") with the actual val
builder.Append("<h2 class=\"sectiontext\">Forecasted Spend - $9,814.81</h2>");
(the preliminary html has been added prior to this). After that I add live data like so:
builder.Append("<table>");
builder.Append("<tr>");
builder.Append("<th>Item Code</th>");
builder.Append("<th class=\"rightjustifytext\">Last Week's Usage</th>");
builder.Append("<th class=\"rightjustifytext\">This Week's Price</th>");
builder.Append("<th class=\"rightjustifytext\">Forecasted Spend</th>");
builder.Append("</tr>");
decimal forecastedSpend;
int lastWeeksUsage;
int loopCount = 0;
int totalUsage = 0;
decimal totalForecastedSpend = 0.0M;
foreach (DataRow dr in dtViewPriceMatrixVarianceResults.Rows)
{
builder.Append("<tr>");
builder.Append(String.Format("<td>{0}</td>", dr["ItemCode"]));
lastWeeksUsage = ProActWebReportsConstsAndUtils.RandomNumber(1, 1000);
totalUsage = totalUsage + lastWeeksUsage;
builder.Append(String.Format("<td class=\"rightjustifytext\">{0}</td>", ProActWebReportsConstsAndUtils.AddThousandsSeparator(lastWeeksUsage, false)));
builder.Append(String.Format("<td class=\"rightjustifytext\">{0}</td>", ProActWebReportsConstsAndUtils.MoneyFormat(Convert.ToDecimal(dr["Price"]), true)));
decimal currentPrice = Convert.ToDecimal(dr["Price"]);
forecastedSpend = lastWeeksUsage * currentPrice;
totalForecastedSpend = totalForecastedSpend + forecastedSpend;
builder.Append(String.Format("<td class=\"rightjustifytext\">{0}</td>", ProActWebReportsConstsAndUtils.MoneyFormat(Convert.ToDecimal(forecastedSpend), true)));
builder.Append("</tr>");
loopCount++;
if (loopCount >= 9) break;
}
(I add the total vals to a total row after this). At the end I close the elements and pass back the contents of the StringBuilder:
builder.Append("</table>");
builder.Append("</div>");
builder.Append("</div>");
return builder.ToString();
The problem is that I don't know the value of totalForecastedSpend (which is the value I need to replace teh dummy "$9,814.81" with) until I populate the html table table rows.tabledata. I could do the loop first, and save the values in a generic list for use in the loop, but I'm wondering/hoping if there is a way to replace that value with the real one when I arrive at it, using either html and/or css to accomplish it. Is it possible and, if so, how? Or would "go back to" be considered harmful?
The REST method is:
[Route("{unit}/{begdate}/{enddate}")]
public HttpResponseMessage GetQuadrantData(string unit, string begdate, string enddate)
{
_unit = unit;
_beginDate = begdate;
_endDate = enddate;
string beginningHtml = GetBeginningHTML();
string top10ItemsPurchasedHtml = GetTop10ItemsPurchasedHTML();
string pricingExceptionsHtml = GetPricingExceptionsHTML();
string forecastedSpendHtml = GetForecastedSpendHTML();
string deliveryPerformanceHtml = GetDeliveryPerformanceHTML();
string endingHtml = GetEndingHTML();
String HtmlToDisplay = string.Format("{0}{1}{2}{3}{4}{5}",
beginningHtml,
top10ItemsPurchasedHtml,
pricingExceptionsHtml,
forecastedSpendHtml,
deliveryPerformanceHtml,
endingHtml);
return new HttpResponseMessage()
{
Content = new StringContent(
HtmlToDisplay,
Encoding.UTF8,
"text/html"
)
};
}
The html-generating code at the top of this post is from the GetForecastedSpendHTML() helper method.
Is there a trick I can do to replace "$9,814.81" with the value later assigned to totalForecastedSpend, or am I forced to pursue some other option?
Note: Attempts to add javascript/jQuery have failed (see here, if interested) so I don't know if a javascript solution would help me here.
UDPATE
This doesn't answer my question, but my idea does work, to loop through and make the calculations and save the vals to a generic list first, and use that thereafter:
First, I created this class:
public class ForecastedSpend
{
public String ItemCode { get; set; }
public int LastWeeksUsage { get; set; }
public Decimal Price { get; set; }
public Decimal ForecastedSpendCalcd { get; set; }
}
...then replaced the code above with this:
int loopCount = 0;
int totalUsage = 0;
decimal totalForecastedSpend = 0.0M;
List<ForecastedSpend> fsList = new List<ForecastedSpend>();
foreach (DataRow dr in dtViewPriceMatrixVarianceResults.Rows)
{
ForecastedSpend fs = new ForecastedSpend();
fs.ItemCode = dr["ItemCode"].ToString();
fs.LastWeeksUsage = ProActWebReportsConstsAndUtils.RandomNumber(1, 1000);
totalUsage = totalUsage + fs.LastWeeksUsage;
decimal currentPrice = Convert.ToDecimal(dr["Price"]);
fs.Price = currentPrice;
fs.ForecastedSpendCalcd = fs.LastWeeksUsage * currentPrice;
totalForecastedSpend = totalForecastedSpend + fs.ForecastedSpendCalcd;
loopCount++;
fsList.Add(fs);
if (loopCount >= 9) break;
}
//builder.Append("<h2 class=\"sectiontext\">Forecasted Spend - $9,814.81</h2>");
builder.Append(string.Format("<h2 class=\"sectiontext\">Forecasted Spend - {0}</h2>", ProActWebReportsConstsAndUtils.MoneyFormat(totalForecastedSpend, true)));
builder.Append("<table>");
builder.Append("<tr>");
builder.Append("<th>Item Code</th>");
builder.Append("<th class=\"rightjustifytext\">Last Week's Usage</th>");
builder.Append("<th class=\"rightjustifytext\">This Week's Price</th>");
builder.Append("<th class=\"rightjustifytext\">Forecasted Spend</th>");
builder.Append("</tr>");
foreach (ForecastedSpend fs in fsList)
{
builder.Append("<tr>");
builder.Append(String.Format("<td>{0}</td>", fs.ItemCode));
builder.Append(String.Format("<td class=\"rightjustifytext\">{0}</td>", ProActWebReportsConstsAndUtils.AddThousandsSeparator(fs.LastWeeksUsage, false)));
builder.Append(String.Format("<td class=\"rightjustifytext\">{0}</td>", ProActWebReportsConstsAndUtils.MoneyFormat(Convert.ToDecimal(fs.Price), true)));
builder.Append(String.Format("<td class=\"rightjustifytext\">{0}</td>", ProActWebReportsConstsAndUtils.MoneyFormat(Convert.ToDecimal(fs.ForecastedSpendCalcd), true)));
builder.Append("</tr>");
}
It works just dandy.

Another -standard- way to transform a result after generating it and before rendering it into the browser is producing an XML result and let the browser transform it with a proper XSL stylesheet.
This technologhy has several advantages:
It puts format apart from data, which is generally a good practice.
It reduces bandwidth, because the XSL is delivered just once by device (and after, it usually gets cached locally).
The transformation is done natively by the browser, which in practice is usually as fast as direct HTML rendering.
It helps to maintainability: Shall the format be changed, the server endpoint program has not to be modified at all; just the XSL stylesheet.

If you are asking for a way to alter elements order in css/html, it sounds to me like a task for flexible box layout.
Example:
HTML:
<div id="mycontainer">
<table id="mytable">
<tr><td>row 1</td></tr>
<tr><td>row 2</td></tr>
<tr><td>row 3</td></tr>
<tr><td>row 4</td></tr>
<tr><td>row 5</td></tr>
<tr><td>row 6</td></tr>
<tr><td>row 7</td></tr>
</table>
<p id="mytotal">2000$</p>
</div>
Then, the CSS:
div#mycontainer {
display: flex;
flex-direction: column;
}
p#mytotal {
flex: 1;
order: 1;
}
table#mytable {
flex: 1;
order: 2;
}
Note that I've set the table to appear in 2nd position, and totals to apper in 1st (thorugh the attribute order).

Related

Warning messages with EZAPI EzDerivedColumn and input columns

When adding a derived column to a data flow with ezAPI, I get the following warnings
"Add stuff here.Inputs[Derived Column Input].Columns[ad_zip]" on "Add
stuff here" has usage type READONLY, but is not referenced by an
expression. Remove the column from the list of available input
columns, or reference it in an expression.
I've tried to delete the input columns, but either the method is not working or I'm doing it wrong:
foreach (Microsoft.SqlServer.Dts.Pipeline.Wrapper.IDTSInputColumn100 col in derFull.Meta.InputCollection[0].InputColumnCollection)
{
Console.WriteLine(col.Name);
derFull.DeleteInputColumn(col.Name);
}
I have the following piece of code that fixes the problem.
I got it from a guy called Daniel Otykier. So he is propably the one that should be credited for it... Unlesss he got it from someone else :-)
static public void RemoveUnusedInputColumns(this EzDerivedColumn component)
{
var usedLineageIds = new HashSet<int>();
// Parse all expressions used in new output columns, to determine which input lineage ID's are being used:
foreach (IDTSOutputColumn100 column in component.GetOutputColumns())
{
AddLineageIdsFromExpression(column.CustomPropertyCollection, usedLineageIds);
}
// Parse all expressions in replaced input columns, to determine which input lineage ID's are being used:
foreach (IDTSInputColumn100 column in component.GetInputColumns())
{
AddLineageIdsFromExpression(column.CustomPropertyCollection, usedLineageIds);
}
var inputColumns = component.GetInputColumns();
// Remove all input columns not used in any expressions:
for (var i = inputColumns.Count - 1; i >= 0; i--)
{
if (!usedLineageIds.Contains(inputColumns[i].LineageID))
{
inputColumns.RemoveObjectByIndex(i);
}
}
}
static private void AddLineageIdsFromExpression(IDTSCustomPropertyCollection100 columnProperties, ICollection<int> lineageIds)
{
int lineageId = 1;
var expressionProperty = columnProperties.Cast<IDTSCustomProperty100>().FirstOrDefault(p => p.Name == "Expression");
if (expressionProperty != null)
{
// Input columns used in expressions are always referenced as "#xxx" where xxx is the integer lineage ID.
var expression = expressionProperty.Value.ToString();
var expressionTokens = expression.Split(new[] { ' ', ',', '(', ')' });
foreach (var c in expressionTokens.Where(t => t.Length > 1 && t.StartsWith("#") && int.TryParse(t.Substring(1), out lineageId)))
{
if (!lineageIds.Contains(lineageId)) lineageIds.Add(lineageId);
}
}
}
Simple but not 100% Guaranteed Method
Call ReinitializeMetaData on the base component that EzApi is extending:
dc.Comp.ReinitializeMetaData();
This doesn't always respect some of the customizations and logic checks that EzAPI has, so test it carefully. For most vanilla components, though, this should work fine.
100% Guaranteed Method But Requires A Strategy For Identifying Columns To Ignore
You can set the UsageType property of those VirtualInputColumns to the enumerated value DTSUsageType.UT_IGNORED using EzApi's SetUsageType wrapper method.
But! You have to do this after you're done modifying any of the other metadata of your component (attaching other components, adding new input or output columns, etc.) since each of these triggers the ReinitializeMetaData method on the component, which automatically sets (or resets) all UT_IGNORED VirtualInputColumn's UsageType to UT_READONLY.
So some sample code:
// define EzSourceComponent with SourceColumnToIgnore output column, SomeConnection for destination
EzDerivedColumn dc = new EzDerivedColumn(this);
dc.AttachTo(EzSourceComponent);
dc.Name = "Errors, Go Away";
dc.InsertOutputColumn("NewDerivedColumn");
dc.Expression["NewDerivedColumn"] = "I was inserted!";
// Right here, UsageType is UT_READONLY
Console.WriteLine(dc.VirtualInputCol("SourceColumnToIgnore").UsageType.ToString());
EzOleDbDestination d = new EzOleDbDestination(f);
d.Name = "Destination";
d.Connection = SomeConnection;
d.Table = "dbo.DestinationTable";
d.AccessMode = AccessMode.AM_OPENROWSET_FASTLOAD;
d.AttachTo(dc);
// Now we can set usage type on columns to remove them from the available inputs.
// Note the false boolean at the end.
// That's required to not trigger ReinitializeMetadata for usage type changes.
dc.SetUsageType(0, "SourceColumnToIgnore", DTSUsageType.UT_IGNORED, false);
// Now UsageType is UT_IGNORED and if you saved the package and viewed it,
// you'll see this column has been removed from the available input columns
// ... and the warning for it has gone away!
Console.WriteLine(dc.VirtualInputCol("SourceColumnToIgnore").UsageType.ToString());
I was having exactly your problem and found a way to solve it. The problem is that the EzDerivedColumn has not the PassThrough defined in it's class.
You just need to add this to the class:
private PassThroughIndexer m_passThrough;
public PassThroughIndexer PassThrough
{
get
{
if (m_passThrough == null)
m_passThrough = new PassThroughIndexer(this);
return m_passThrough;
}
}
And alter the ReinitializeMetadataNoCast() to this:
public override void ReinitializeMetaDataNoCast()
{
try
{
if (Meta.InputCollection[0].InputColumnCollection.Count == 0)
{
base.ReinitializeMetaDataNoCast();
LinkAllInputsToOutputs();
return;
}
Dictionary<string, bool> cols = new Dictionary<string, bool>();
foreach (IDTSInputColumn100 c in Meta.InputCollection[0].InputColumnCollection)
cols.Add(c.Name, PassThrough[c.Name]);
base.ReinitializeMetaDataNoCast();
foreach (IDTSInputColumn100 c in Meta.InputCollection[0].InputColumnCollection)
{
if (cols.ContainsKey(c.Name))
SetUsageType(0, c.Name, cols[c.Name] ? DTSUsageType.UT_READONLY : DTSUsageType.UT_IGNORED, false);
else
SetUsageType(0, c.Name, DTSUsageType.UT_IGNORED, false);
}
}
catch { }
}
That is the strategy used by other components. If you want to see all the code you can check my EzApi2016#GitHub. I'm updating the original code from Microsoft to SQL Server 2016.

Adding attribute to checkbox list at runtime

So I have some code that dynamically creates an ASP.NET form based on an XML input file. I'm trying to add attributes to the controls at run time and I'm having some weird issues with list items.
My Server Side Code looks something like this:
Me.radioButtonList = New RadioButtonList()
Me.dropDownList = New DropDownList()
Me.listControl = Nothing
If controlType = "dropdown" Then
Me.listControl = Me.dropDownList
Else
Me.listControl = Me.radioButtonList
End If
For Each ansElement As Answer In strAnswers
Dim newListItem = New ListItem(ansElement.AnswerText, ansElement.AnswerText)
If ansElement.ActionID IsNot Nothing AndAlso ansElement.ActionID <> "" Then
newListItem.Attributes.Add("actionID", ansElement.ActionID)
End If
Me.listControl.Items.Add(newListItem)
Next
Me.listControl.ID = controlID
Me.Controls.Add(Me.listControl)
The problem is when I run the code and the page is render the attributes are being added to the proceeding span tag of the control not the input item itself. So the rendered HTML ends up looking like this.
<span actionID="1">
<input id="lst_dynamic_MedIllnesses_0" name="ctl00$MainContentPlaceHolder$FormGenerator1$lst_dynamic_MedIllnesses$lst_dynamic_MedIllnesses_0" value="None" type="checkbox">
<label for="lst_dynamic_MedIllnesses_0">None</label>
</span>
What do I have to do to get the actionID attribute to be added to the actual input control and not the span tag?
Thanks!
I suppose you are talking about RadioButtonList. The problem with it is that it uses RadioButton control, and it has 3 attributes properties - Attributes, InputAttributes and LabelAttributes. Each of them is used for specific html element.
The problem with RadioButtonList, is that it uses just Attributes property, and doesn't use InputAttributes. Here is code of RadioButtonList.RenderItem method:
protected virtual void RenderItem(ListItemType itemType, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer)
{
if (repeatIndex == 0)
{
this._cachedIsEnabled = this.IsEnabled;
this._cachedRegisterEnabled = this.Page != null && !this.SaveSelectedIndicesViewState;
}
RadioButton controlToRepeat = this.ControlToRepeat;
int index1 = repeatIndex + this._offset;
ListItem listItem = this.Items[index1];
controlToRepeat.Attributes.Clear();
if (listItem.HasAttributes)
{
foreach (string index2 in (IEnumerable) listItem.Attributes.Keys)
controlToRepeat.Attributes[index2] = listItem.Attributes[index2];
}
if (!string.IsNullOrEmpty(controlToRepeat.CssClass))
controlToRepeat.CssClass = "";
ListControl.SetControlToRepeatID((Control) this, (Control) controlToRepeat, index1);
controlToRepeat.Text = listItem.Text;
controlToRepeat.Attributes["value"] = listItem.Value;
controlToRepeat.Checked = listItem.Selected;
controlToRepeat.Enabled = this._cachedIsEnabled && listItem.Enabled;
controlToRepeat.TextAlign = this.TextAlign;
controlToRepeat.RenderControl(writer);
if (!controlToRepeat.Enabled || !this._cachedRegisterEnabled || this.Page == null)
return;
this.Page.RegisterEnabledControl((Control) controlToRepeat);
}
controlToRepeat is that RadioButton, and it specifies only Attributes property and ignores InputAttributes.
I can suggest way to fix it - you can create new class that inherits RadioButtonList, and use it instead of default. Here is code of that class:
public class MyRadioButtonList : RadioButtonList
{
private bool isFirstItem = true;
protected override void RenderItem(ListItemType itemType, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer)
{
if (isFirstItem)
{
// this.ControlToRepeat will be created during this first call, and then it will be placed into Controls[0], so we can get it from here and update for each item.
var writerStub = new HtmlTextWriter(new StringWriter());
base.RenderItem(itemType, repeatIndex, repeatInfo, writerStub);
isFirstItem = false;
}
var radioButton = this.Controls[0] as RadioButton;
radioButton.InputAttributes.Clear();
var item = Items[repeatIndex];
foreach (string attribute in item.Attributes.Keys)
{
radioButton.InputAttributes.Add(attribute, item.Attributes[attribute]);
}
// if you want to clear attributes for top element, in that case it's a span, then you need to call
item.Attributes.Clear();
base.RenderItem(itemType, repeatIndex, repeatInfo, writer);
}
}
A bit of description - it has isFirstItem property, as RadioButton control that used by it is created in runtime in the first access, so we need to call RenderItem before we can update InputAttrubutes property. So we call it once and send some stub HtmlTextWriter, so it won't be displayed twice. And then after that we just get this control as Controls[0], and for each ListItem we update InputAttributes values.
PS. Sorry, I didn't use VB.Net so control is written in C#

Create a Non-Database-Driven Lookup

Lots of references for creating lookups out there, but all seem to draw their values from a query.
I want to add a lookup to a field that will add items from a list of values that do not come from a table, query, or any other data source.
Such as from a string: "Bananas, Apples, Oranges"
..or a container ["Bananas", "Apples", "Oranges"]
Assume the string/container is a dynamic object. Drawing from an static enum is not a choice.
Is there a way to create lookups on the fly from something other than a data source?
Example code would be a great help, but I'll take hints as well.
There is the color picker.
Also in the Global you will find pickXxxx such as pickList.
There are others, pickUser, pickUserGroup etc.
Take a look on the implementation. I guess they build a temporary table then displays that. Tables are great!
Update:
To go on you own follow the rules.
For the advanced user, see also: Lookup form returning more than one value.
public void lookup()
{
SysTableLookup sysTableLookup;
TmpTableFieldLookup tmpTableFieldLookup;
Enumerator en;
List entitylist = new list(types::String);
entitylist.addend("Banana");
entitylist.addend("Apple");
en = entityList.getEnumerator();
while (en.moveNext())
{
tmpTableFieldLookup.TableName = en.current();
tmpTableFieldLookup.insert();
}
sysTableLookup = SysTableLookup::newParameters(tableNum(tmpTableFieldLookup), this);
sysTableLookup.addLookupfield(fieldNum(TmpTableFieldLookup, TableName));
//BP Deviation documented
sysTableLookup.parmTmpBuffer(tmpTableFieldLookup);
sysTableLookup.performFormLookup();
}
The above code helps in displaying strings as lookup.
I'm also guessing there's no way to perform a lookup without a table. I say that because a lookup is simply a form with one or more datasources that is displayed in a different way.
I've also blogged about this, so you can get some info on how to perform a lookup, even with a temporary table, here:
http://devexpp.blogspot.com.br/2012/02/dynamics-ax-custom-lookup.html
Example from global::PickEnumValue:
static int pickEnumValue(EnumId _enumId, boolean _omitZero = false)
{
Object formRun;
container names;
container values;
int i,value = -1,valueIndex;
str name;
#ResAppl
DictEnum dictEnum = new DictEnum(_enumId);
;
if (!dictEnum)
return -1;
for (i=1;i<=dictEnum.values();i++)
{
value = dictEnum.index2Value(i);
if (!(_omitZero && (value == 0)))
{
names += dictEnum.index2Label(i);
values += value;
}
}
formRun = classfactory.createPicklist();
formRun.init();
formRun.choices(names, #ImageClass);
formRun.caption(dictEnum.label());
formRun.run();
formRun.wait();
name = formRun.choice();
value = formRun.choiceInt();
if (value>=0) // the picklist form returns -1 if a choice has not been made
{
valueIndex = -1;
for (i=1;i<=conLen(names);i++)
{
if (name == conPeek(names,i))
{
valueIndex = i;
break;
}
}
if (valueIndex>=0)
return conPeek(values,valueIndex);
}
return value;
}
It isn't the most graceful solution, but this does work, and it doesn't override or modify any native AX 2012 objects:
Copy the sysLookup form from AX2009 (rename it) and import it into AX 2012.
We'll call mine myLookupFormCopy.
I did a find/replace of "sysLookup" in the XPO file to rename it.
Create this class method:
public static client void lookupList(FormStringControl _formStringControl, List _valueList, str _columnLabel = '')
{
Args args;
FormRun formRun;
;
if (_formStringControl && _valueList && _valueList.typeId() == Types::String)
{
args = new Args(formstr(myLookupFormCopy));
args.parmObject(_valueList);
args.parm(_columnLabel);
formRun = classFactory.formRunClass(args);
_formStringControl.performFormLookup(formRun);
}
}
In the lookup method for your string control, use:
public void lookup()
{
List valueList = new List(Types::String);
;
...build your valueList here...
MyClass::lookupList(this, valueList, "List Title");
super();
}

In WatiN, how to verify a table's column headers and rows?

Consider this HTML table:
<table id="build-table">
<tr>
<th>Build ID</th>
<th>Build Time</th>
</tr>
<tr>
<td>
5.1
</td>
<td>02.06.2011 13:33:03</td>
</tr>
</table>
How would I verify in WatiN that the table has the correct headers (Build ID and Build Time), and the correct content (in this case, one row containing the given hyperlink and date string)?
Sorry, we created a custom TableHandler, using the basic table building blocks: Here is the sample code:
public TableController(Regex tableControlId)
{ InitializeMembers(Find.ById(tableControlId), true); }
private void InitializeMembers(WatiN.Core.Constraints.AttributeConstraint tableControlId, bool hasColumnHeaders)
{
if (tableControlId == null)
{
throw new ArgumentNullException("tableControlId", "'tableControlId' passed in should not be null.");
}
WatiN.Core.Constraints.AttributeConstraint newTableControlId = tableControlId;
Assert.IsTrue(IE.Table(newTableControlId).Exists, "Table with id '" + newTableControlId.ToString() + "' does not exist on this page.");
_controlId = tableControlId;
_hasColumnHeaders = hasColumnHeaders;
_columnHeaders = (hasColumnHeaders) ? GetTableColumnHeaders() : null;
_totalRows = Table.TableRows.Count;
_totalColumns = GetAllColumnDataFromRow((TableRow)Table.TableRows[0], hasColumnHeaders).Count;
}
private StringCollection GetTableColumnHeaders()
{
return GetAllColumnDataFromRow((TableRow)Table.TableRows[0], true);
}
private StringCollection GetAllColumnDataFromRow(TableRow tableRow, bool isTableHeaderRow)
{
StringCollection RowValues = new StringCollection();
if (tableRow == null)
{
for (int colCounter = 0; colCounter < this.TotalColumns; colCounter++) RowValues.Add(String.Empty);
}
if (isTableHeaderRow)
{
foreach (Element e in tableRow.Elements)
{
if (e.TagName == "TH")
{
RowValues.Add(e.Text);
}
}
}
else
{
foreach (TableCell tc in tableRow.TableCells)
{
if (String.IsNullOrEmpty(tc.Text))
{
RowValues.Add(String.Empty);
}
else
{
RowValues.Add(tc.Text);
}
}
}
//fill up for the missing cells, if any, with blanks
int actualCellsInRow = tableRow.TableCells.Count;
int expectedCellsInRow = this.TotalColumns;
for (int colCounter = actualCellsInRow; colCounter < expectedCellsInRow; colCounter++)
{
RowValues.Add(String.Empty);
}
return RowValues;
}
Hope this helps.
There are at least three ways to do this:
I think Watin provides a tablehandler / tablecontroller related methods using which you can retrieve this information. You might want to search on that topic if you want to take this approach.
Using Xpath query (XML). Since you have the id of the table, you can use a XPath query to reach the node where your header is and verify that using a static string "Build ID" in your code. Same with the other pieces of information.
Regex - Using Regular Expressions, you can check if that text exists on the control / page.
If you view the source of your page, you will know the pattern that you should look for on the page. In fact, a simple Assert.AreEqual(true, new Regex("Build ID")Match.Success) should do the trick. However, this is a check that is purely done to see if the text exists on the page. You will not be looking at anything beyond that. Also, if you have multiple occurrences of the text then you should be considering the array of matches you get before you say, "yep, found it".
Note: You may have to checkout the syntax for using a Regex. The above information is just an abstract of what it would look like.
Cheers.

Trim string to length ignoring HTML

This problem is a challenging one. Our application allows users to post news on the homepage. That news is input via a rich text editor which allows HTML. On the homepage we want to only display a truncated summary of the news item.
For example, here is the full text we are displaying, including HTML
In an attempt to make a bit more space in the office, kitchen, I've pulled out all of the random mugs and put them onto the lunch room table. Unless you feel strongly about the ownership of that Cheyenne Courier mug from 1992 or perhaps that BC Tel Advanced Communications mug from 1997, they will be put in a box and donated to an office in more need of mugs than us.
We want to trim the news item to 250 characters, but exclude HTML.
The method we are using for trimming currently includes the HTML, and this results in some news posts that are HTML heavy getting truncated considerably.
For instance, if the above example included tons of HTML, it could potentially look like this:
In an attempt to make a bit more space in the office, kitchen, I've pulled...
This is not what we want.
Does anyone have a way of tokenizing HTML tags in order to maintain position in the string, perform a length check and/or trim on the string, and restore the HTML inside the string at its old location?
Start at the first character of the post, stepping over each character. Every time you step over a character, increment a counter. When you find a '<' character, stop incrementing the counter until you hit a '>' character. Your position when the counter gets to 250 is where you actually want to cut off.
Take note that this will have another problem that you'll have to deal with when an HTML tag is opened but not closed before the cutoff.
Following the 2-state finite machine suggestion, I've just developed a simple HTML parser for this purpose, in Java:
http://pastebin.com/jCRqiwNH
and here a test case:
http://pastebin.com/37gCS4tV
And here the Java code:
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class HtmlShortener {
private static final String TAGS_TO_SKIP = "br,hr,img,link";
private static final String[] tagsToSkip = TAGS_TO_SKIP.split(",");
private static final int STATUS_READY = 0;
private int cutPoint = -1;
private String htmlString = "";
final List<String> tags = new LinkedList<String>();
StringBuilder sb = new StringBuilder("");
StringBuilder tagSb = new StringBuilder("");
int charCount = 0;
int status = STATUS_READY;
public HtmlShortener(String htmlString, int cutPoint){
this.cutPoint = cutPoint;
this.htmlString = htmlString;
}
public String cut(){
// reset
tags.clear();
sb = new StringBuilder("");
tagSb = new StringBuilder("");
charCount = 0;
status = STATUS_READY;
String tag = "";
if (cutPoint < 0){
return htmlString;
}
if (null != htmlString){
if (cutPoint == 0){
return "";
}
for (int i = 0; i < htmlString.length(); i++){
String strC = htmlString.substring(i, i+1);
if (strC.equals("<")){
// new tag or tag closure
// previous tag reset
tagSb = new StringBuilder("");
tag = "";
// find tag type and name
for (int k = i; k < htmlString.length(); k++){
String tagC = htmlString.substring(k, k+1);
tagSb.append(tagC);
if (tagC.equals(">")){
tag = getTag(tagSb.toString());
if (tag.startsWith("/")){
// closure
if (!isToSkip(tag)){
sb.append("</").append(tags.get(tags.size() - 1)).append(">");
tags.remove((tags.size() - 1));
}
} else {
// new tag
sb.append(tagSb.toString());
if (!isToSkip(tag)){
tags.add(tag);
}
}
i = k;
break;
}
}
} else {
sb.append(strC);
charCount++;
}
// cut check
if (charCount >= cutPoint){
// close previously open tags
Collections.reverse(tags);
for (String t : tags){
sb.append("</").append(t).append(">");
}
break;
}
}
return sb.toString();
} else {
return null;
}
}
private boolean isToSkip(String tag) {
if (tag.startsWith("/")){
tag = tag.substring(1, tag.length());
}
for (String tagToSkip : tagsToSkip){
if (tagToSkip.equals(tag)){
return true;
}
}
return false;
}
private String getTag(String tagString) {
if (tagString.contains(" ")){
// tag with attributes
return tagString.substring(tagString.indexOf("<") + 1, tagString.indexOf(" "));
} else {
// simple tag
return tagString.substring(tagString.indexOf("<") + 1, tagString.indexOf(">"));
}
}
}
You can try the following npm package
trim-html
It cutting off sufficient text inside html tags, save original html stricture, remove html tags after limit is reached and closing opened tags.
If I understand the problem correctly, you want to keep the HTML formatting, but you want to not count it as part of the length of the string you are keeping.
You can accomplish this with code that implements a simple finite state machine.
2 states: InTag, OutOfTag
InTag:
- Goes to OutOfTag if > character is encountered
- Goes to itself any other character is encountered
OutOfTag:
- Goes to InTag if < character is encountered
- Goes to itself any other character is encountered
Your starting state will be OutOfTag.
You implement a finite state machine by procesing 1 character at a time. The processing of each character brings you to a new state.
As you run your text through the finite state machine, you want to also keep an output buffer and a length so far encountered varaible (so you know when to stop).
Increment your Length variable each time you are in the state OutOfTag and you process another character. You can optionally not increment this variable if you have a whitespace character.
You end the algorithm when you have no more characters or you have the desired length mentioned in #1.
In your output buffer, include characters you encounter up until the length mentioned in #1.
Keep a stack of unclosed tags. When you reach the length, for each element in the stack, add an end tag. As you run through your algorithm you can know when you encounter a tag by keeping a current_tag variable. This current_tag variable is started when you enter the InTag state, and it is ended when you enter the OutOfTag state (or when a whitepsace character is encountered while in the InTag state). If you have a start tag you put it in the stack. If you have an end tag, you pop it from the stack.
Here's the implementation that I came up with, in C#:
public static string TrimToLength(string input, int length)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
if (input.Length <= length)
return input;
bool inTag = false;
int targetLength = 0;
for (int i = 0; i < input.Length; i++)
{
char c = input[i];
if (c == '>')
{
inTag = false;
continue;
}
if (c == '<')
{
inTag = true;
continue;
}
if (inTag || char.IsWhiteSpace(c))
{
continue;
}
targetLength++;
if (targetLength == length)
{
return ConvertToXhtml(input.Substring(0, i + 1));
}
}
return input;
}
And a few unit tests I used via TDD:
[Test]
public void Html_TrimReturnsEmptyStringWhenNullPassed()
{
Assert.That(Html.TrimToLength(null, 1000), Is.Empty);
}
[Test]
public void Html_TrimReturnsEmptyStringWhenEmptyPassed()
{
Assert.That(Html.TrimToLength(string.Empty, 1000), Is.Empty);
}
[Test]
public void Html_TrimReturnsUnmodifiedStringWhenSameAsLength()
{
string source = "<div lang=\"en\" class=\"textBody localizable\" id=\"pageBody_en\">" +
"<img photoid=\"4041\" src=\"http://xxxxxxxx/imagethumb/562103830000/4041/300x300/False/mugs.jpg\" style=\"float: right;\" class=\"photoRight\" alt=\"\"/>" +
"<br/>" +
"In an attempt to make a bit more space in the office, kitchen, I";
Assert.That(Html.TrimToLength(source, 250), Is.EqualTo(source));
}
[Test]
public void Html_TrimWellFormedHtml()
{
string source = "<div lang=\"en\" class=\"textBody localizable\" id=\"pageBody_en\">" +
"<img photoid=\"4041\" src=\"http://xxxxxxxx/imagethumb/562103830000/4041/300x300/False/mugs.jpg\" style=\"float: right;\" class=\"photoRight\" alt=\"\"/>" +
"<br/>" +
"In an attempt to make a bit more space in the office, kitchen, I've pulled out all of the random mugs and put them onto the lunch room table. Unless you feel strongly about the ownership of that Cheyenne Courier mug from 1992 or perhaps that BC Tel Advanced Communications mug from 1997, they will be put in a box and donated to an office in more need of mugs than us. <br/><br/>" +
"In the meantime we have a nice selection of white Ikea mugs, some random Starbucks mugs, and others that have made their way into the office over the years. Hopefully that will suffice. <br/><br/>" +
"</div>";
string expected = "<div lang=\"en\" class=\"textBody localizable\" id=\"pageBody_en\">" +
"<img photoid=\"4041\" src=\"http://xxxxxxxx/imagethumb/562103830000/4041/300x300/False/mugs.jpg\" style=\"float: right;\" class=\"photoRight\" alt=\"\"/>" +
"<br/>" +
"In an attempt to make a bit more space in the office, kitchen, I've pulled out all of the random mugs and put them onto the lunch room table. Unless you feel strongly about the ownership of that Cheyenne Courier mug from 1992 or perhaps that BC Tel Advanced Communications mug from 1997, they will be put in";
Assert.That(Html.TrimToLength(source, 250), Is.EqualTo(expected));
}
[Test]
public void Html_TrimMalformedHtml()
{
string malformedHtml = "<div lang=\"en\" class=\"textBody localizable\" id=\"pageBody_en\">" +
"<img photoid=\"4041\" src=\"http://xxxxxxxx/imagethumb/562103830000/4041/300x300/False/mugs.jpg\" style=\"float: right;\" class=\"photoRight\" alt=\"\"/>" +
"<br/>" +
"In an attempt to make a bit more space in the office, kitchen, I've pulled out all of the random mugs and put them onto the lunch room table. Unless you feel strongly about the ownership of that Cheyenne Courier mug from 1992 or perhaps that BC Tel Advanced Communications mug from 1997, they will be put in a box and donated to an office in more need of mugs than us. <br/><br/>" +
"In the meantime we have a nice selection of white Ikea mugs, some random Starbucks mugs, and others that have made their way into the office over the years. Hopefully that will suffice. <br/><br/>";
string expected = "<div lang=\"en\" class=\"textBody localizable\" id=\"pageBody_en\">" +
"<img photoid=\"4041\" src=\"http://xxxxxxxx/imagethumb/562103830000/4041/300x300/False/mugs.jpg\" style=\"float: right;\" class=\"photoRight\" alt=\"\"/>" +
"<br/>" +
"In an attempt to make a bit more space in the office, kitchen, I've pulled out all of the random mugs and put them onto the lunch room table. Unless you feel strongly about the ownership of that Cheyenne Courier mug from 1992 or perhaps that BC Tel Advanced Communications mug from 1997, they will be put in";
Assert.That(Html.TrimToLength(malformedHtml, 250), Is.EqualTo(expected));
}
I'm aware this is quite a bit after the posted date, but i had a similiar issue and this is how i ended up solving it. My concern would be the speed of regex versus interating through an array.
Also if you have a space before an html tag, and after this doesn't fix that
private string HtmlTrimmer(string input, int len)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
if (input.Length <= len)
return input;
// this is necissary because regex "^" applies to the start of the string, not where you tell it to start from
string inputCopy;
string tag;
string result = "";
int strLen = 0;
int strMarker = 0;
int inputLength = input.Length;
Stack stack = new Stack(10);
Regex text = new Regex("^[^<&]+");
Regex singleUseTag = new Regex("^<[^>]*?/>");
Regex specChar = new Regex("^&[^;]*?;");
Regex htmlTag = new Regex("^<.*?>");
while (strLen < len)
{
inputCopy = input.Substring(strMarker);
//If the marker is at the end of the string OR
//the sum of the remaining characters and those analyzed is less then the maxlength
if (strMarker >= inputLength || (inputLength - strMarker) + strLen < len)
break;
//Match regular text
result += text.Match(inputCopy,0,len-strLen);
strLen += result.Length - strMarker;
strMarker = result.Length;
inputCopy = input.Substring(strMarker);
if (singleUseTag.IsMatch(inputCopy))
result += singleUseTag.Match(inputCopy);
else if (specChar.IsMatch(inputCopy))
{
//think of as 1 character instead of 5
result += specChar.Match(inputCopy);
++strLen;
}
else if (htmlTag.IsMatch(inputCopy))
{
tag = htmlTag.Match(inputCopy).ToString();
//This only works if this is valid Markup...
if(tag[1]=='/') //Closing tag
stack.Pop();
else //not a closing tag
stack.Push(tag);
result += tag;
}
else //Bad syntax
result += input[strMarker];
strMarker = result.Length;
}
while (stack.Count > 0)
{
tag = stack.Pop().ToString();
result += tag.Insert(1, "/");
}
if (strLen == len)
result += "...";
return result;
}
Wouldn't the fastest way be to use jQuery's text() method?
For example:
<ul>
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
var text = $('ul').text();
Would give the value OneTwoThree in the text variable. This would allow you to get the actual length of the text without the HTML included.