Getting an object back from my GridView rows - asp.net

Basically, i want my object back...
I have an Email object.
public class Email{
public string emailAddress;
public bool primary;
public int contactPoint;
public int databasePrimaryKey;
public Email(){}
}
In my usercontrol, i a list of Email objects.
public List<Email> EmailCollection;
And i'm binding this to a GridView inside my usercontrol.
if(this.EmailCollection.Count > 0){
this.GridView1.DataSource = EmailCollection;
this.GridView1.DataBind();
}
It would be really awesome, if i could get an Email object back out of the GridView later.
How do i do this?
I'm also binding only some of the Email object's properties to the GridView as well and they're put into Item Templates.
<Columns>
<asp:TemplateField HeaderText="Email Address">
<ItemTemplate>
<asp:TextBox ID="TextBox1" runat="server" Text=<%# Eval("EmailAddress") %> Width=250px />
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Primary">
<ItemTemplate>
<asp:CheckBox runat="server" Checked=<%# Eval("PrimaryEmail") %> />
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Contact Point">
<ItemTemplate>
<CRM:QualDropDown runat="server" Type=ContactPoint InitialValue=<%# Eval("ContactPoint") %> />
</ItemTemplate>
</asp:TemplateField>
</Columns>
Can GridView even do this? Do i need to roll my own thing? It'd be really cool if it would do it for me.
To elaborate more.
I am saving the List collection into the viewstate.
What I'm eventually trying to get to, is there will be a Save button somewhere in the control, which when the event fires I'd like to create an Email object from a datarow in the GridView which to compare to my original List collection. Then if there's a change, then I'd update that row in the database. I was thinking that if I could put a List collection into a GridView, then perhaps I could get it right back out.
Perhaps I create a new constructor for my Email object which takes a DataRow? But then there's a lot of complexities that goes into that...

ASP.NET Databinding is a one-way operation in terms of object manipulation. However, the DataSource property will contain a reference to your EmailCollection throughout the response:
EmailCollection col = (EmailCollection)this.GridView1.DataSource;
But I have a feeling that what you really want is a control that manipulates your EmailCollection based on user input and retrieve it in the next request. Not even webforms can fake that kind of statefulness out of the box.

Well I ended up looping through my List EmailCollection, which was saved into the ViewState.
So in the page, a Save button is clicked, when the event is caught, I loop through my List Collection and grab the row from the GridView by index.
On the GridViewRow I have to use a GridView1.Rows[i].Cells[j].FindControl("myControl1") then get the appropriate value from it, be it a check box, text box, or drop down list.
I do see that a GridViewRow object has a DataItem property, which contains my Email object, but it's only available during the RowBound phase.
Unfortunately If/When i need to expand upon this Email Collection later, by adding or removing columns, it'll take a few steps.
protected void SaveButton_OnClick(object sender, EventArgs e){
for (int i = 0; i < this.EmailCollection.Count; i++)
{
Email email = this.EmailCollection[i];
GridViewRow row = this.GridView1.Rows[i];
string gv_emailAddress = ((TextBox)row.Cells[0].FindControl("EmailAddress")).Text;
if (email.EmailAddress != gv_emailAddress)
{
email.EmailAddress = gv_emailAddress;
email.Updated = true;
}
...
}
}
I'd still be open to more efficient solutions.

Just a thought, basically a roll your own but not that tricky to do:
Store the list that you use as a datasource in the viewstate or session, and have a hidden field in the gridview be the index or a key to the object that matches the row.
In other words, each row in the gridview "knows" which email object in the list that it is based on.

If you want to hold onto an object like this its easiest to use the viewstate, although you will be duplicating the data but for a small object it should be ok.
ViewState.Add("EmailObj", Email);
EMail email = (Email)ViewState["EmailObj"];

Related

How to avoid repetition of data in Gridview?

In a web application I am binding the data to a GridView. In the GridView some of data is repeating. I want to not display the data again and again.
For example Empid is displaying more than one time in the same column. I want to not display the empid again in that column.
You can implement the OnDataBinding event for the specific column you are using. I never use AutoGenerateColumns so having fine control of each cell is pretty simple to implement.
Eg:
// Create global in your .cs file
string _currentEmpID = string.Empty;
Define your column like:
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:Literal ID="ltEmpID" runat="server"
OnDataBinding="ltEmpID_DataBinding" />
</ItemTemplate>
</asp:TemplateField>
<!-- Your other columns... -->
</Columns>
Then just implement your DataBinding event:
protected void ltEmpID_DataBinding(object sender, System.EventArgs e)
{
Literal lt = (Literal)(sender);
string empID = Eval("EmpID").ToString();
if (!empID.Equals(_currentEmpID))
{
lt.Text = empID;
_currentEmpID = empID;
}
else
{
lt.Text = string.Empty;
}
}
The RowDataBound forces you to search for controls and if changes are required in the future you have the possibility of breaking other things being modified within the event. Because of this, I prefer to use the control's DataBinding event whenever possible as it localizes functionality to only the control and gives you the flexability to swap out controls and functionality easily without the worry off affecting other things.
If you group your data by the columns you don't want to repeat before binding it to your datasource you can bind an event to RowDataBound and check if the current value equals the previous and then hide the cell.
Check this for an example.
Just add the property AutoGenerateColumns in the gridview and assign it the value of false.
AutoGenerateColumns="false"

Creating a Custom Button in a ListView in ASP.NET

I have a Results.aspx page that displays the resulting records queried using a SqlDataSource object via a ListView. I want to add a "View" button that will appear next to each record, and when clicked will take me to a separate page that will display details about that record. How do I accomplish this?
Edit
I have tried what you said, citronas and here's what I've come up with:
<td>
<asp:CheckBox ID="CheckBox1" runat="server" />
</td>
<td>
<asp:LinkButton ID="LinkButton1" runat="server" CommandName="ViewButtonClick" CommandArgument='<%# Eval("ServiceId") %>'>View</asp:LinkButton>
</td>
And here is the method that I want to be called:
protected void ViewButtonClick(object sender, CommandEventArgs e)
{
var serviceId = Convert.ToInt32(e.CommandArgument);
ServiceToView = DataAccessLayer.Service.Select(new Service { ServiceId = serviceId });
Server.Transfer("~/ViewService.aspx");
}
Unfortunately nothing actually happens...am I missing something?
Edit -- Fixed
I was missing something! I had CommandName equal to my method name instead of OnCommand. I took out CommandName, kept the argument bit and replaced CommandName with OnCommand. Everything works now, but what would I ever need CommandName for?
You can add a LinkButton into the ItemTemplate of the ListView.
Bind the value that identifies each record to the CommandArgument of the LinkButton.
Subscribe to the Command-Event of the LinkButton. There you have access to CommandEventArgs.CommandArgument
What you wound up doing worked Storm. I decided to go with Citronas' suggestion and share my answer.
FIRST:
On the aspx I added a LinkButton to my ItemTemplate with my own CommandName and CommandArgument. I passed my item's ID as a CommandArgument so I could later use it inside my sub.
<asp:LinkButton ID="lnkBtnAnswers" runat="server" CommandName="Answers"
CommandArgument='<%# Eval("ID")%>'>Answers</asp:LinkButton>
SECOND:
On the codebehind I created a sub that would be called whenever the user conducted an action. As Citronas mentioned normally you use "Select", "Add", "Edit", or "Delete" here. I decided to create "answers".
Note: Handles MyControl.ItemCommand is very important here as it is what subscribes you to the command event.
Protected Sub lvQuestions_Command(sender As Object, e As CommandEventArgs) Handles lvQuestions.ItemCommand
If e.CommandName.ToLower() = "answers" Then
hfSelectedQuestionID.Value = e.CommandArgument
End If
End Sub
Done! Now since every command goes through the new sub, it is important to check for the right commandName so you can conduct the appropriate action. Don't forget to use the CommandArgument to your advantage.

Creating dynamic DataList controls with ID's based on bound data

As a workaround for the fact that asp:Checkboxes don't have values, I am attempting to dynamically create the ID's of checkboxes in a DataList so that it inserts the primary keys into the control ID. This is surprisingly difficult.
I have placed a PlaceHolder in my DataList ItemTemplate, then in the ItemCreated I create the checkboxes using string.Format("Checkbox{0}", DataBinder(e.Item.DataItem, "ID")). The problem is that this only works in a non-postback condition, as on postback the DataItem is null. And of course ItemDataBound isn't called on PostBack so that won't work either.
I can't seem to find a good way to handle this short of if (IsPostback) dataList.Bind(), which i don't think is a good way to do it.
Can anyone provide me with any alternatives here?
EDIT:
Some additional information. I just realized that part of the problem was because I actually have a DataList within a DataList. The reason DataItem is null is because there is no databinding on postback, and the child data is not saved to viewstate.
Basically, what i'm doing is This, although it's using a DataList rather than Repeater. So, on postback, the Children collection doesn't get set because ItemDataBound isn't called on postback.
EDIT2: To clarify, the problem is largely because of the nested datalists. I have to set the datasource of the nested datalist to a collection field of the first datalist's individual rows fields. On postback, there is no databinding, so it doesn't work.
You could use a similar technique to the one I wrote up in this answer - add a regular CheckBox, and a HiddenField control in the ItemTemplate, and bind the HiddenField to the primary key value e.g.
<ItemTemplate>
<tr>
<td>
<asp:CheckBox runat="server" ID="MyCheckBox" AutoPostBack="true" oncheckedchanged="MyCheckBox_CheckedChanged" />
<asp:HiddenField runat="server" id="DatabaseKeyHiddenField" Value='<%# Eval("DatabaseKey") %>' />
</td>
</tr>
</ItemTemplate>
protected void MyCheckBox_CheckedChanged(object sender, EventArgs e)
{
CheckBox selectedCheckBox;
DataListItem selectedDataListItem;
HiddenField databaseKeyHiddenField;
string databaseKey;
// Cast the sender object to a CheckBox
selectedCheckBox = (CheckBox)sender;
// Walk up the tree one level so we get the container for both controls
selectedDataListItem = (DataListItem)selectedCheckBox.Parent;
// Get the HiddenField control ...
databaseKeyHiddenField = (HiddenField)selectedDataListItem.FindControl("DatabaseKeyHiddenField");
// ... and read the value
databaseKey = databaseKeyHiddenField.Value;
// Go off and do a database update based on the key we now have
...
}
It's a bit of a workaround rather than exactly what you want to do, but it works!

How do I get many property values from View to Presenter in WebFormsMvp?

What is the best way to get a number of property values of a business object from the View to the Presenter in a WebFormsMvp page?
Bearing in mind this issue with DataSources.
Here is what i propose:
The scenario is, I have a business object called Quote which i would like to load form the database, edit and then save. The Quote class has heaps of properties on it. The form is concerned with about 20 of these properties. I have existing methods to load/save a Quote object to/from the database. I now need to wire this all together.
So, in the View_Load handler on my presenter i intend to do something like this:
public void View_Load(object sender, EventArgs e)
{
View.Model.Quote = quoteService.Read(quoteId);
}
And then bind all my controls as follows:
<asp:TextBox ID="TotalPriceTextBox" runat="server"
Text="<%# Model.Quote.TotalPrice %>" />
All good, the data is on the screen.
The user then makes a bunch of changes and hits a "Submit" button. Here is where I'm unsure.
I create a class called QuoteEventArgs exposing the 20 properties the form is able to edit. When the View raises the Submit button's event, I set these properties to the values of the controls in the code behind. Then raise the event for the presenter to respond to. The presenter re-loads the Quote object from the database, sets all the properties and saves it to the database.
Is this the right way to do this? If not, what is?
"A nicer way" (/alternative) is to make use of the 2-way binding, therefore what will be passed back to the Presenter for processing will be your Quote object.
This can be achieved through the use of an asp:FormView in conjunction with the mvp:PageDataSource that specifies an UpdateMethod and the Bind() method.
The WebFormsMVP sample project demonstrates this via the 'EditWidgetControl', including the methods required on the View code-behind file.
As an option your view can simply implement only the EditItemTemplate for asp:FormView making use of DefaultMode="Edit" on the FormView.
Sample Structure:
<asp:FormView DataSourceID="theSource" DefaultMode="Edit">
<EditItemTemplate>
<fieldset>
<asp:TextBox id="totp" value='<%# Bind("TotalPrice") %>' runat="server" />
</fieldset>
</EditItemTemplate>
</asp:FormView>
<mvp:PageDataSource ID="theSource" runat="server"
DataObjectTypeName="Your.NameSpace.Quote"
UpdateMethod="UpdateQuote">
</mvp:PageDataSource>
Code-behind:
public void UpdateQuote(Quote q, Quote ori)
{
OnUpdatingQuote(q, ori);
}
public event EventHandler<UpdateQuoteEventArgs> UpdatingQuote;
private void OnUpdatingQuote(Quote q, Quote ori)
{
if (UpdatingUserGroup != null)
{
UpdatingUserGroup(this, new UpdateQuoteEventArgs(q, ori));
}
}
How to use the GridView inside a FormView.
Because I have list to populate the grid in a entity.

How to set the RowStyle of a GridView row depending on a property of the Object that the row is being bound to

I'm currently using a GridView and I want to set the CssClass for the Row depending on a property of the object that the row is being bound to.
I tried the following but it does not work (see comments):
<asp:GridView id="searchResultsGrid" runat="server" AllowPaging="true" PageSize="20" AutoGenerateColumns="false">
<!-- The following line doesn't work because apparently "Code blocks
aren't allowed in this context: -->
<RowStyle CssClass="<%#IIF(DataBinder.Eval(Container.DataItem,"NeedsAttention","red","") %>
<Columns>
<!--............-->
</Columns>
</asp:GridView>
Now I could simply handle the GridView's RowDataBound event and change the css class of the row there...but I'm trying to keep a clear separation between the UI and the page/business logic layers.
I have no idea how to accomplish this and I'm looking forward to hearing any suggestions.
Thanks,
-Frinny
You cannot do this in declarative markup.
Nearly all of GridView's declarative properties (including GridView.RowStyle) are grid-level settings rather than row-level. Apart from TemplateFields , they are not bound data containers, so they don't have access to the data in their rows.
If you want to keep this logic in the .aspx template, your only real option is to use template fields and manipulate their contents:
<asp:TemplateField>
<ItemTemplate>
<span class="<%# ((string)Eval("property3")) == "NeedsAttention" ? "red" : string.Empty %>">
<%# Eval("property1") %>
</span>
</ItemTemplate>
</asp:TemplateField>
Depending on what you want to do, this may be awkward - you don't have access to the containing <td> (or <tr> for that matter) and you'll have to repeat the formatting for each cell.
The GridView class goes to a lot of lengths to hide the details of HTML and styling from you. After all you could create a GridView control adapter that wouldn't even render as HTML tables. (Unlikely though that may be.)
So even though you're trying to avoid it, you're probably best off dealing with this in a OnRowDataBound handler - or use a Repeater (if that's appropriate).
I know it has been almost a year, but if anyone else is trying this, try to subclass the GridView.
public class GridViewCSSRowBindable : GridView
{
public string DataFieldRowCSSClass { get; set; }
protected override void OnRowDataBound(GridViewRowEventArgs e)
{
base.OnRowDataBound(e);
if (!string.IsNullOrEmpty(DataFieldRowCSSClass))
{
//This will throw an exception if the property does not exist on the data item:
string cssClassString = DataBinder.Eval(e.Row.DataItem, DataFieldRowCSSClass) as string;
if (!string.IsNullOrEmpty(cssClassString))
{
string sep = string.IsNullOrEmpty(e.Row.CssClass) ? string.Empty : " ";
e.Row.CssClass += sep + cssClassString;
}
}
}
}
And then in your Page:
<custom:GridViewCSSRowBindable ID="gvExample" runat="server" DataFieldRowCSSClass="RowCSS">
</custom:GridViewCSSRowBindable>
The objects being bound to this example GridView should have a public string RowCSS property.
If you haven't used inherited controls before, you might have to look up how to set that up in your project.
foreach (TableCell gvc in gvRowPhistry.Cells)
{
gvc.ForeColor = System.Drawing.Color.Blue;
}

Resources