I have an asp.net program that creates a simple survey form for users to answer.
Most of the questions use a dropdownlist with answer scores from 1-5 (bad-good) and I'm trying to add an event handler to the dropdownlist objects so that the comments box is only enabled if the user selects a score between 1 and 2.
However when I add the delegate lambda call for the event handler, instead of each dropdownlist affecting their own corresponding comment box, they all seem to point only at the last one added (and they work once, then no more and only the last ddl continues having the expected behaviour).
My code:
//Called from Page_Load
private void PopulateSurvey()
{
btnSubmit.Enabled = true;
List<Question> questions = (from p in context.Questions
join q in context.Survey_Questions on p.ID equals q.QuestionID
where q.SurveyID == surveyid
select p).ToList();
Table tbl = new Table();
tbl.Width = Unit.Percentage(100);
TableRow tr;
TableCell tc;
TableCell tc1;
TableCell tc2;
TextBox txt;
CheckBox cbk;
DropDownList ddl = new DropDownList();
foreach (Question q in questions)
{
if (q.Division.Equals("General") || q.Division.Equals(ddlDivisions.SelectedValue.ToString()))
{
tr = new TableRow();
tc = new TableCell();
tc.Width = Unit.Percentage(55);
tc.Text = q.Text;
tc.Attributes.Add("id", q.ID.ToString());
tr.Cells.Add(tc);
tc = new TableCell();
if (q.QuestionType.ToLower() == "singlelinetextbox")
{
txt = new TextBox();
txt.ID = "txt_" + q.ID;
//txt.Width = Unit.Percentage(40);
tc.Controls.Add(txt);
}
if (q.QuestionType.ToLower() == "multilinetextbox")
{
txt = new TextBox();
txt.ID = "txt_" + q.ID;
txt.TextMode = TextBoxMode.MultiLine;
//txt.Width = Unit.Percentage(40);
tc.Controls.Add(txt);
}
if (q.QuestionType.ToLower() == "singleselect")
{
ddl = new DropDownList();
ddl.ID = "ddl_" + q.ID;
//ddl.Width = Unit.Percentage(41);
if (!string.IsNullOrEmpty(q.Options))
{
string[] values = q.Options.Split(',');
foreach (string v in values)
ddl.Items.Add(v.Trim());
}
ddl.AutoPostBack = true;
tc.Controls.Add(ddl);
}
//tc.Width = Unit.Percentage(60);
tr.Cells.Add(tc);
//Add comment row
tc1 = new TableCell();
//tc.Width = Unit.Percentage(5);
tc1.Text = "Comentario: ";
tc1.Attributes.Add("id", q.ID.ToString());
//tc1.Visible = false;
tr.Cells.Add(tc1);
tc2 = new TableCell();
txt = new TextBox();
txt.ID = "txt_" + q.ID + "comment";
txt.TextMode = TextBoxMode.SingleLine;
//txt.Width = Unit.Percentage(25);
tc2.Controls.Add(txt);
tr.Cells.Add(tc2);
ddl.SelectedIndexChanged+= (sender, e) => ScoreChanged(sender, e, tc1,tc2, ddl.SelectedIndex);
tbl.Rows.Add(tr);
}
}
pnlSurvey.Controls.Add(tbl);
}
protected void ScoreChanged (object sender, EventArgs e, TableCell tc1, TableCell tc2, int score)
{
if( score <2)
{
tc1.Visible = false;
tc2.Visible = false;
}
else
{
tc1.Visible = true;
tc2.Visible = true;
}
}
First thing that comes to mind is maybe the event handler keeps the references themselves,m instead of the orphaned objects created with each question, so all event handlers end up with the same tc1 and tc2 reference and thus only the last object? Is that the case and if so how do I go around it?
Your problem is in this line:
ddl.SelectedIndexChanged +=
(sender, e) => ScoreChanged(sender, e, tc1,tc2, ddl.SelectedIndex);
Two things are important here:
You are using an anonymous method to invoke ScoreChanged.
You are passing tc1 and tc2 as an argument to the ScoreChanged method. You have defined those variables at the beginning of your code block, outside the loop.
The magic word in this context is closure. Since tc1 and tc2 are defined outside the scope of the anonymous method, they turn into closures. That means the will not have the value at the time you define the method, but at the time you invoke it. Since you are constantly overwriting the value of the variables in the foreach loop, at the time of the invokation those variables will have the value of the last iteration of the loop.
The solution is simple: Declare the variables inside the loop. This will create a new closure for each iteration of the foreach:
TableRow tr;
TableCell tc;
TextBox txt;
CheckBox cbk;
foreach (Question q in questions)
{
TableCell tc1;
TableCell tc2;
DropDownList ddl; //Don't forget to include ddl, since you are using its selected index
//...
On a more general note: Don't do "declaration" blocks like this at the beginning of a method in C#. Declare the variable the first time you use it (unless there is a good reason to do otherwise, for example you want it to be a part of a closure). There are many good reasons for that and you just have experienced one of them. Another one would be that when you turn a part of your code to a method with Visual Studio's refactoring feature, you will pass the predeclared variables as ref params. These are the most obvious reasons. There are more.
Related
Below is the code that creates a table and dynamically shows items from a database. This work great, I would like to add a row on a onclick event from a button and make the data persistent when I click the button and not lose data. Any help would be appreciated.
Table tb = new Table();
tb.BorderWidth = 1;
tb.Width = new Unit("740px");
tb.BorderStyle = BorderStyle.Solid;
tb.ID = "myTable";
TableRow tr = new TableRow();
TableRow tr2 = new TableRow();
TableRow trEndRow = new TableRow();
foreach (QuestionBuilder QB in GetQuestionsDatabySection)
{
TableCell tc1 = new TableCell();
tc1.Text = OQB.QuestionText;
tc1.BorderWidth = 1;
tr.Cells.Add(tc1);
tb.Rows.Add(tr);
TableCell tc2 = new TableCell();
tc2.Controls.Add(getControl(OQB.DynamicAttributeId, "0"));
tc2.BorderWidth = 1;
tr2.Cells.Add(tc2);
tb.Rows.Add(tr2);
TableCell tcEnd = new TableCell();
Button btn = new Button();
btn.Text = "Add Row";
btn.Attributes.Add("OnClick", "ButtonAddRow_Click");
btn.Attributes.Add("runat", "server");
tcEnd.Controls.Add(btn);
tcEnd.BorderWidth = 1;
trEndRow.Cells.Add(tcEnd);
tb.Rows.Add(trEndRow);
// Add to PlaceHolder
phTable.Controls.Add(tb);
}
Since you're doing a web app in ASP.NET you'll need to use something to manage your state.
Here's a good breakdown from Microsoft on your options: https://msdn.microsoft.com/en-us/library/z1hkazw7.aspx
Depending on the complexity of your data, view state and query strings are pretty common implementations, but that article highlights the advantages and disadvantages of each option. Once you've decided on which option you want to go with, it should be pretty easy to Google an example of how it's used.
Theoretically you could even reload the data from the database on each post back, if you were inclined to do so.
I am writing a web application in Visual Studio 2015 pro using C# and ASP.NET. Right now, I have it set up where the user will click a button and the C# code will go get a bunch of data then display it back to the user in tables. I have spent a day of work trying to figure out how to add some form of a clickable event to the table rows but have had no success. Ultimately, what I want to do is call a method in my C# code when the table row is clicked and send it the row index.
Here is the C# code I am using for generating the tables:
protected void searchButton_Click(object sender, EventArgs e)
{
try
{
// Remove the error display
resultListLabel.Style.Add("Display", "None");
// Get document groups
groups = TableCommands.getGroups(dbConn, "retriever", searchTextOne.Text, searchTextTwo.Text);
foreach (var dataPair in groups)
{
// Get data pair group names into list
List<string> name = dataPair.Key.Split('|').ToList();
// ====== Make table
Table resultTable = new Table();
resultTable.Attributes["class"] = "displayTable";
resultList.Controls.Add(resultTable);
// ====== Add table info row
TableRow groupInfo = new TableRow();
groupInfo.Attributes["class"] = "groupInfoLabel";
// add row to table
resultTable.Rows.Add(groupInfo);
// create cell with information
TableCell infoCell = new TableCell();
infoCell.Text = "MRN: "+name[0]+", Name: " + name[1];
infoCell.ColumnSpan = 3;
// add cell to row
groupInfo.Cells.Add(infoCell);
// ====== Make column label row
TableRow labelRow = new TableRow();
labelRow.Attributes["class"] = "columnLabel";
// add row to table
resultTable.Rows.Add(labelRow);
// make an array of column lables
string[] cellNames = new string[] { "Visit Date", "Document Type", "Doctor ID" };
// add column lables to row
foreach (string s in cellNames)
{
TableCell labelCell = new TableCell();
labelCell.Text = s;
labelRow.Cells.Add(labelCell);
}
// Add display names to table
foreach(var nameList in dataPair.Value)
{
TableRow nameRow = new TableRow();
nameRow.Attributes["class"] = "columnInfo";
for (int i = 0; i < 3; i++)
{
TableCell nameCell = new TableCell();
nameCell.Text = nameList[i];
nameRow.Cells.Add(nameCell);
}
resultTable.Rows.Add(nameRow);
}
}
}
catch(Exception ex)
{
// Display the error and write to log
resultListLabel.Style.Add("Display", "Inline-Block");
writeLog("Failed to generate tables", ex.ToString());
}
}
Here's what I had to do:
Add a global boolean to called tableCall to the C# code
Move the code in searchButton_Click into an if(tableCall) statement in the Page_Load method.
protected void Page_Load(object sender ,EventArgs e){
...
if(tableCall){
//Do stuff from searchButton_Click
}
...
}
Add tableCall = true; and Page_Load(sender, e) to searchButton_Click
Modify the for loop to add buttons in the cell like so:
// Add display names to table
foreach (var nameList in dataPair.Value)
{
TableRow nameRow = new TableRow();
nameRow.Attributes["class"] = "columnInfo";
// Add display names to table
foreach (var nameList in dataPair.Value)
{
TableRow nameRow = new TableRow();
nameRow.Attributes["class"] = "columnInfo";
for (int i = 0; i < 3; i++)
{
TableCell nameCell = new TableCell();
nameRow.Cells.Add(nameCell);
Button b = new Button();
b.Attributes["class"] = "docButton";
b.Attributes.Add("DWdocid", nameList[3]);
b.Text = nameList[i];
b.Click += new EventHandler((s, ea) => test(s, ea, b.Attributes["DWdocid"]));
nameCell.Controls.Add(b);
}
resultTable.Rows.Add(nameRow);
}
This adds a button to each of the three cells in the row, but adds the same document id to each of the buttons in that row, so anywhere the user clicks on that row (except for the 1px borders) will call a method in the C# code while passing it the document ID. I'm sure that with better css skills somebody could make the button span all three cells.
I think I'm doing something terribly wrong here. I have a object inside import called oCultivationPlan. It contains data obviously. And I want to create a table which shows the data inside it. However I only want a selected few from that object and not all the data in it. Is there a way to make this shorter? I thought about using foreach or for, but that would loop through all the data inside the object :/ while I only want a selected few.
TableRow tblRow = new TableRow();
TableCell tblc = new TableCell();
tblc.Controls.Add(new LiteralControl("ID"));
TableCell tblc2 = new TableCell();
tblc2.Controls.Add(new LiteralControl(import.oCultivationPlan.iID.ToString()));
tblRow.Controls.Add(tblc);
tblRow.Controls.Add(tblc2);
tblImportPreview.Controls.Add(tblRow);
TableCell tblc3 = new TableCell();
TableCell tblc4 = new TableCell();
tblc3.Controls.Add(new LiteralControl("Description"));
tblc4.Controls.Add(new LiteralControl(import.oCultivationPlan.sDescription.ToString()));
TableRow tblRow2 = new TableRow();
tblRow2.Controls.Add(tblc3);
tblRow2.Controls.Add(tblc4);
tblImportPreview.Controls.Add(tblRow2);
TableCell tblc5 = new TableCell();
TableCell tblc6 = new TableCell();
tblc5.Controls.Add(new LiteralControl("DateCreated"));
tblc6.Controls.Add(new LiteralControl(import.oCultivationPlan.dDateCreated.ToString()));
TableRow tblRow3 = new TableRow();
tblRow3.Controls.Add(tblc5);
tblRow3.Controls.Add(tblc6);
tblImportPreview.Controls.Add(tblRow3);
not a foreach :) but you can use a for loop to get trough it. you should be able to use the code below as a solution for your question :)
its smaller cuz of the loop but it does the exact same thing as what you did. I use the string array to keep all the info you want to get inside the table so that it will be having something to go out after.
For each row you have you got 2 new cells in it and thats why we have the row*2 so the cells can get filled up :)
Hope it works for you and that you can use the solution :)
int _row = 1;
int _cell = 0;
string[] arr = new string[6] { "ID", import.oCultivationPlan.iID.ToString(), "Description", import.oCultivationPlan.sDescription.ToString(), "DateCreated", import.oCultivationPlan.dDateCreated.ToString() };
for (; _row <= 3; _row++)
{
TableRow tblRow = new TableRow();
for (; _cell < _row * 2; _cell++)
{
TableCell tblc = new TableCell();
tblc.Controls.Add(new LiteralControl(arr[_cell]));
tblRow.Controls.Add(tblc);
}
tblImportPreview.Controls.Add(tblRow);
}
I would create a strong typed Class
public Class ImportDto
{
public string RowIdentifier {get; set;}
public string RowValue {get; set;
}
Then as David said, write a filter function to filter data from Import class and map it to ImportValues
public List<ImportDto> FilterImportedData(Import import)
{
var importDto = new List<ImportDto>
{
new ImportDto { RowIdentifier ="ID", RowValue = import.oCultivationPlan.iID.ToString()},
new ImportDto { RowIdentifier ="Description", RowValue = import.oCultivationPlan.sDescription.ToString()}
};
}
Then in the aspx.cs class, just loop through List<ImportDto>and create LiteralControls
foreach(var dto in importDtos)
{
var row = new TableRow();
var identifierCell = new TableCell();
var valueCell = new TableCell();
identifierCell.Controls.Add(new LiteralControl(dto.RowIdentifier));
valueCell.Controls.Add(new LiteralControl(dto.RowValue ));
row.Add(identifierCell);
row.Add(valueCell);
tblImportPreview.Controls.Add(row);
}
That way all you need to do in future to add new filtered data, is to modify your mapping function and add a new ImportDto, and the it will be displayed in the frontend automatically.
Edit for clarity: I know the code has 2 columns, one for title and one for date, I am trying to change the code so it displays 3 events per row.
I am new to development and I am trying to take an ASP control used in a Sitefinity 6.1 website that currently takes data from our SQL database and outputs it into a single column and instead produce an output in 3 columns.
The .ascx looks like this
<div style="margin:10px">
<asp:MultiView ID="mvEvents" runat="server" ActiveViewIndex="0">
<asp:View ID="viewDefault" runat="server">
<asp:Table ID="tblEvents" runat="server" CellPadding="5">
</asp:Table>
</asp:View>
<asp:View ID="viewList" runat="server">
<asp:Table ID="tblEventsList" runat="server" CellPadding="5">
</asp:Table>
</asp:View>
</asp:MultiView>
</div>
The PageLoad and BuildEvents portions of the .ascx.cs code look like this
protected void Page_Load(object sender, EventArgs e)
{
try
{
CalendarDataContext db = new CalendarDataContext();
var evt = db.CalendarEvents(NumberToDisplay, Department);
foreach (CalendarEventsResult Evt in evt)
{
if (Department == "List")
{
BuildEventsList(Evt.event_name, Evt.event_start_date, Evt.event_idn, Evt.information_id);
}
else
{
BuildEvents(Evt.event_name, Evt.event_start_date, Evt.event_idn, Evt.information_id);
}
}
return;
}
catch(Exception ex)
{
}
}
protected void BuildEvents(string EvtTitle, DateTime EvtStart, int EvtIdn, int EvtInfoIdn)
{
//EvtInfoIdn shows Event Description without location. EvtInfoIdn - 1 shows location information. changing the href to eventInfoId
int EvtInfoId = EvtInfoIdn - 1;
TableRow tr = new TableRow();
tr.VerticalAlign = VerticalAlign.Top;
tr.HorizontalAlign = HorizontalAlign.Center;
TableCell tcTitle = new TableCell();
TableCell tcStart = new TableCell();
StringBuilder sb = new StringBuilder();
sb.AppendLine(ShortMonth(EvtStart.Month));
sb.AppendLine(EvtStart.Day.ToString());
tr.VerticalAlign = VerticalAlign.Bottom;
tr.Height = Unit.Pixel(80);
Literal litDate = new Literal();
litDate.Text = "<div class='EventDate'>" + sb.ToString() + "</div>";
tcStart.Controls.Add(litDate);
Literal litTitle = new Literal();
litTitle.Text = #"<div class='EventTitle'><a style='text-decoration:none;' href='http://events.website.edu/EventList.aspx?view=EventDetails&eventidn=" + EvtIdn.ToString().Trim() + "&information_id=" + EvtInfoId + "&type=&rss=rss'>";
litTitle.Text = litTitle.Text + EvtTitle + "</a></div>";
tcTitle.Controls.Add(litTitle);
tr.Cells.Add(tcStart);
tr.Cells.Add(tcTitle);
tblEvents.Rows.Add(tr);
}
I have tried adding a ColumnSpan = "3" attribute to the ascx file and also tried adding the ColumnSpan to tcStart and tcTitle in the cs file as well in addition to trying to redesign the ascx in design mode and nothing I have though of has worked. I realize this is likely an easy fix, but I am new to C# programming. I appreciate any help provided.
Try the following -- n.b. I haven't been able to test fully as I haven't mocked up your bespoke classes, e.g. CalendarDataContext.
The basic premise is that the CalandarEvents collection is processed in batches of 3 (or whatever number you choose) and that the BuildEvents method now is broken down into smaller parts, each with their own responsibility, which should help with maintenance going forward.
protected void Page_Load(object sender, EventArgs e)
{
try
{
CalendarDataContext db = new CalendarDataContext();
var evt = db.CalendarEvents(NumberToDisplay, Department);
if (Department == "List")
{
foreach (CalendarEventsResult cer in evt)
{
BuildEventsList(cer.event_name, cer.event_start_date, cer.event_idn, cer.information_id);
}
}
else
{
// Depending what type evt is may depend on how to convert to the List.
// This assumes that it supports IEnumerable (like DataTable)
List<CalendarEventsResult> events = evt.AsEnumerable().ToList();
// Rather than hard-coding, read this from config or database
// to allow easier changing to a different number of events per row?
int numberOfEventsPerRow = 3;
// loop increments in steps of 3 (numberOfEventsPerRow)
for (int rowNumber = 0; rowNumber < events.Count; rowNumber += numberOfEventsPerRow)
{
// use Linq to pick out the subset of Events to process
List<CalendarEventsResult> queryEvents = events
.Skip(numberOfEventsPerRow * rowNumber)
.Take(numberOfEventsPerRow).ToList<CalendarEventsResult>();
// Build a row using that subset
TableRow tr = buildRow(queryEvents, numberOfEventsPerRow);
// Add it to the table
tblEvents.Rows.Add(tr);
}
}
}
catch (Exception ex)
{
// this just suppresses any exceptions. Better to fix issues at source.
}
}
private TableRow buildRow(List<CalendarEventsResult> events, int eventsPerRow)
{
TableRow tr = new TableRow();
// Can these be added to CSS?
tr.HorizontalAlign = HorizontalAlign.Center;
tr.VerticalAlign = VerticalAlign.Bottom;
tr.Height = Unit.Pixel(80);
TableCell tc;
// for our event subset, build a pair of cells for each Event,
// adding them to a row
foreach (var evt in events)
{
tc = BuildEventDate(evt);
tr.Cells.Add(tc);
tc = BuildEventTitle(evt);
tr.Cells.Add(tc);
}
// If we're dealing with a partial row, i.e. only 1 or 2 Events,
// pad the row with empty cells.
if (events.Count < eventsPerRow)
{
tc = BuildEmptyCell(eventsPerRow - events.Count);
tr.Cells.Add(tc);
}
return tr;
}
private TableCell BuildEventDate(CalendarEventsResult evt)
{
TableCell tc = new TableCell();
Literal litDate = new Literal();
//String.Format should suffice c.f. StringBuilder
litDate.Text = String.Format("<div class='EventDate'>{0}{1}</div>", ShortMonth(evt.Month), evt.Day.ToString());
tc.Controls.Add(litDate);
return tc;
}
private TableCell BuildEventTitle(CalendarEventsResult evt)
{
TableCell tc = new TableCell();
int evtInfoId = evt.information_id - 1;
Literal litTitle = new Literal();
// n.b. hard-coded URL doesn't look good for ease of maintenance (move to config or CalendarEventsResult?)
litTitle.Text = String.Format(#"<div class='EventTitle'><a style='text-decoration:none;' href='http://events.website.edu/EventList.aspx?view=EventDetails&eventidn={0}&information_id={1}&type=&rss=rss'>{2}</a></div>",
evt.event_idn.ToString().Trim(), evtInfoId, evt.event_name);
tc.Controls.Add(litTitle);
return tc;
}
private TableCell BuildEmptyCell(int numberOfEvents)
{
// Define as const rather than having unexplained "2" hard-coded.
const int spanPerEvent = 2;
TableCell tc = new TableCell();
tc.ColumnSpan = spanPerEvent * numberOfEvents;
tc.Text = "";
return tc;
}
}
I've been trying with no success to create a shopping cart in c# asp.net and SQL server.
I was able to retrieve the products from Database and to create a detail view for each product as well.
Right now I need to create the button "add to cart", I know that I have to create a session variable with an array to save the products information and then show it in the cart but I don't know how.
This is my code in productsDetail.cs:
protected void Page_Load(object sender, EventArgs e)
{
SqlConnection connexionBD = new SqlConnection(this.data_un_jouet.ConnectionString);
SqlCommand commandeJouets = null;
SqlDataReader lecteurJouets = null;
try
{
connexionBD.Open();
String id = Request.QueryString["no"];
Session["product"] = id;
string myQuery = data_un_jouet.SelectCommand;
commandeJouets = new SqlCommand(myQuery, connexionBD);
commandeJouets.Parameters.Add("#id", SqlDbType.Int).Value = Convert.ToInt32(id);
lecteurJouets = commandeJouets.ExecuteReader();
TableRow uneLigne;
TableCell uneCellule;
while (lecteurJouets.Read())
{
uneLigne = new TableRow();
string myID = Convert.ToString(lecteurJouets["id"]);
uneCellule = new TableCell();
uneCellule.Text = Convert.ToString(lecteurJouets["name"]);
uneLigne.Cells.Add(uneCellule);
uneCellule = new TableCell();
uneCellule.Text = "<img src=" + lecteurJouets["image"].ToString() + "/>";
uneLigne.Cells.Add(uneCellule);
uneCellule = new TableCell();
uneCellule.Text = Convert.ToString(lecteurJouets["description"]);
uneLigne.Cells.Add(uneCellule);
uneCellule = new TableCell();
uneCellule.Text = Convert.ToString(lecteurJouets["price"]);
uneLigne.Cells.Add(uneCellule);
uneCellule = new TableCell();
uneCellule.Text = "<a href=\"cart.aspx?no=" + myID + "\" />Add to cart</a>";
uneLigne.Cells.Add(uneCellule);
this.un_jouet.Rows.Add(uneLigne);
}
lecteurJouets.Close();
commandeJouets.Dispose();
}
catch (Exception ex)
{
msgErreur.Text = "Erreur de connexion ! " + ex.Message;
}
finally
{
connexionBD.Close();
}
I know most of the code is in french, I've tried to translate some of it.
This code works just fine, the problem is that I don't know what to do when the "Add to cart button" is clicked.
I've created a session variable but it only contains the product ID.
Thank you very much for your help,
Like you said you need to assign an array to the session. Better yet, use a List since you don't have a fixed number of items. Let's say that we store the IDs of the items in the shopping cart, and that those IDs are ints. So first we need to instantiate the List. There are many places where you can do this, but for now the Page_Load event is fine.
If (Session["ShoppingCart"] == null)
{
Session["ShoppingCart"] = new List<int>();
}
We first check to see if we've already assigned the session variable, and if not we do so.
Then when the customer adds an item to the shopping cart (say presses a button), you need to add this to the code handling that event:
var products = (List<int>)Session["ShoppingCart"];
products.Add(newProductId);
What I haven't done here (and you should) is to check in the session variable actually has a List to avoid errors. Remember that sessions are not strongly typed.
For better and cleaner code, you should encapsulate the session inside it's own get/set property.