LINQ: custom column names - asp.net

UPDATE
I'm basically binding the query to a WinForms DataGridView. I want the column headers to be appropriate and have spaces when needed. For example, I would want a column header to be First Name instead of FirstName.
How do you create your own custom column names in LINQ?
For example:
Dim query = From u In db.Users _
Select u.FirstName AS 'First Name'

As CQ states, you can't have a space for the field name, you can return new columns however.
var query = from u in db.Users
select new
{
FirstName = u.FirstName,
LastName = u.LastName,
FullName = u.FirstName + " " + u.LastName
};
Then you can bind to the variable query from above or loop through it whatever....
foreach (var u in query)
{
// Full name will be available now
Debug.Print(u.FullName);
}
If you wanted to rename the columns, you could, but spaces wouldn't be allowed.
var query = from u in db.Users
select new
{
First = u.FirstName,
Last = u.LastName
};
Would rename the FirstName to First and LastName to Last.

I solved my own problem but all of your answers were very helpful and pointed me in the right direction.
In my LINQ query, if a column name had more than one word I would separate the words with an underscore:
Dim query = From u In Users _
Select First_Name = u.FirstName
Then, within the Paint method of the DataGridView, I replaced all underscores within the header with a space:
Private Sub DataGridView1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles DataGridView1.Paint
For Each c As DataGridViewColumn In DataGridView1.Columns
c.HeaderText = c.HeaderText.Replace("_", " ")
Next
End Sub

If you want to change the header text, you can set that in the GridView definition...
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false">
<Columns>
<asp:BoundField DataField="FirstName" HeaderText="First Name" />
</Columns>
</asp:GridView>
In the code behind you can bind to the users and it will set the header to First Name.
protected void Page_Load(object sender, EventArgs e)
{
// initialize db datacontext
var query = from u in db.Users
select u;
GridView1.DataSource = query;
GridView1.DataBind();
}

You can also add an event handler to replace those underscores for you!
For those of you who love C#:
datagrid1.ItemDataBound +=
new DataGridItemEventHandler(datagrid1_HeaderItemDataBound);
And your handler should look like this:
private void datagrid1_HeaderItemDataBound(object sender, DataGridItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.Header)
{
foreach(TableCell cell in e.Item.Cells)
cell.Text = cell.Text.Replace('_', ' ');
}
}

I would use:
var query = from u in db.Users
select new
{
FirstName = u.FirstName,
LastName = u.LastName,
FullName = u.FirstName + " " + u.LastName
};
(from Scott Nichols)
along with a function that reads a Camel Case string and inserts spaces before each new capital (you could add rules for ID etc.). I don't have the code for that function with me for now, but its fairly simple to write.

You can make your results have underscores in the column name and use a HeaderTemplate in a TemplateField to replace underscores with spaces. Or subclass the DataControlField for the GridView and override the HeaderText property:
namespace MyControls
{
public SpacedHeaderTextField : System.Web.UI.WebControls.BoundField
{ public override string HeaderText
{ get
{ string value = base.HeaderText;
return (value.Length > 0) ? value : DataField.Replace(" ","");
}
set
{ base.HeaderText = value;
}
}
}
}
ASPX:
<%#Register TagPrefix="my" Namespace="MyControls" %>
<asp:GridView DataSourceID="LinqDataSource1" runat='server'>
<Columns>
<my:SpacedHeaderTextField DataField="First_Name" />
</Columns>
</asp:GridView>

I dont see why you would have to do that, if you are trying to do that for a grid or something, why not just name the header in the HTML?

What you would actually be doing is setting a variable reference to the return, there is not a way to name a variable with a space. Is there an end result reason you are doing this, perhaps if we knew the ultimate goal we could help you come up with a solution that fits.

Using Linq Extension Method:
SomeDataSource.Select(i => new { NewColumnName = i.OldColumnName, NewColumnTwoName = i.OldColumnTwoName});

As others have already pointed out, if the header title etc is known at design time, turn off AutoGeneratedColumns and just set the title etc in the field definition instead of using auto generated columns. From your example it appears that the query is static and that the titles are known at design time so that is probably your best choice.
However [, although your question does not specify this requirement] - if the header text (and formatting etc) is not known at design time but will be determined at runtime and if you need to auto generate columns (using AutoGenerateColumns=
true") there are workarounds for that.
One way to do that is to create a new control class that inherits the gridview. You can then set header, formatting etc for the auto generated fields by overriding the gridview's "CreateAutoGeneratedColumn". Example:
//gridview with more formatting options
namespace GridViewCF
{
[ToolboxData("<{0}:GridViewCF runat=server></{0}:GridViewCF>")]
public class GridViewCF : GridView
{
//public Dictionary<string, UserReportField> _fieldProperties = null;
public GridViewCF()
{
}
public List<FieldProperties> FieldProperties
{
get
{
return (List<FieldProperties>)ViewState["FieldProperties"];
}
set
{
ViewState["FieldProperties"] = value;
}
}
protected override AutoGeneratedField CreateAutoGeneratedColumn(AutoGeneratedFieldProperties fieldProperties)
{
AutoGeneratedField field = base.CreateAutoGeneratedColumn(fieldProperties);
StateBag sb = (StateBag)field.GetType()
.InvokeMember("ViewState",
BindingFlags.GetProperty |
BindingFlags.NonPublic |
BindingFlags.Instance,
null, field, new object[] {});
if (FieldProperties != null)
{
FieldProperties fps = FieldProperties.Where(fp => fp.Name == fieldProperties.Name).Single();
if (fps.FormatString != null && fps.FormatString != "")
{
//formatting
sb["DataFormatString"] = "{0:" + fps.FormatString + "}";
field.HtmlEncode = false;
}
//header caption
field.HeaderText = fps.HeaderText;
//alignment
field.ItemStyle.HorizontalAlign = fps.HorizontalAlign;
}
return field;
}
}
[Serializable()]
public class FieldProperties
{
public FieldProperties()
{ }
public FieldProperties(string name, string formatString, string headerText, HorizontalAlign horizontalAlign)
{
Name = name;
FormatString = formatString;
HeaderText = headerText;
HorizontalAlign = horizontalAlign;
}
public string Name { get; set; }
public string FormatString { get; set; }
public string HeaderText { get; set; }
public HorizontalAlign HorizontalAlign { get; set; }
}
}

I believe this can be achieved using explicit name type
system.Name,
sysentity.Name
//change this to
entity = sysentity.Name

My VS2008 is busted right now, so I can't check. In C#, you would use "=" - How about
Dim query = From u In db.Users _
Select 'First Name' = u.FirstName

Related

connection string manage with DropDownList

Suppose we have 5 companies with five database. Now the situation is we need to change the database path[connection string] on company name which is appear in dropdownlist. When we select the company name on selection of dropdown value we need to go to that companies database. How to do this in asp.net and what necessary changes do in web.config and dropdown event of company name.
now i tried:
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
if (DropDownList1.SelectedItem.Text.Equals("RVL LOGISTICS (I) PVT LTD"))
{
string cs = ConfigurationManager.ConnectionStrings["CompMasterConnectionString"].ConnectionString;
}
else if (DropDownList1.SelectedItem.Text.Equals("SIMONS SHIPPING PVT LTD"))
{
string vs = ConfigurationManager.ConnectionStrings["DUM01ConnectionString"].ConnectionString;
}
else
{
DropDownList2.Enabled = false;
}
}
PLease find below code to change the connection string dynamiccaly at runtime based on dropdown list selected text or value as per your need.
<asp:DropDownList ID="ddltest" AutoPostBack="True" OnSelectedIndexChanged="ddltest_OnSelectedIndexChanged" runat="server">
<asp:ListItem Text="CompanyName" Value="1"></asp:ListItem>
<asp:ListItem Text="CompanyName1" Value="2"></asp:ListItem>
</asp:DropDownList>
protected void ddltest_OnSelectedIndexChanged(object sender, EventArgs e)
{
var conString = "";
var ConStringName = "test";
switch (ddltest.SelectedItem.Text.ToLower())
{
case "CompanyName":
conString = GetConStringFromAppSetting("CompanyName");
AddUpdateConnectionString(conString, ConStringName);
break;
case "CompanyName1":
conString = GetConStringFromAppSetting("CompanyName1");
AddUpdateConnectionString(conString, ConStringName);
break;
}
}
private string GetConStringFromAppSetting(string test)
{
var constring = ConfigurationManager.AppSettings[test].Trim();
return constring;
}
private void AddUpdateConnectionString(string conStringValue, string conStringName)
{
var path = Server.MapPath("~/Web.Config");
var doc = new XmlDocument();
doc.Load(path);
if (doc.DocumentElement != null)
{
var list = doc.DocumentElement.SelectNodes(string.Format("connectionStrings/add[#name='{0}']", conStringName));
if (list != null)
{
var node = list[0];
if (node.Attributes != null) node.Attributes["connectionString"].Value = conStringValue;
}
}
doc.Save(path);
}
in Above code i changed your if else block to switch block, which helps to read code nicely. It will check the selected text value from the dropdown.
First Method is GetConStringFromAppSetting. It will accept the selected Text of dropdown and it will check the same in AppSetting Section of WebConfig and get the value from that.
In the AppSetting of Web Config do work like below:
<appSettings>
<add key="CompanyName" value="constringOfThatCompany" />
<add key="CompanyName1" value="constringOfThatCompany1" />
</appSettings>
After that there is method is AddUpdateConnectionString. IT will accept the Constring value which is recevied from from above method and default connection string name.
After that in that method:
Load the config file.
Load that file in XMLDOcument so we can parse easily.
3.Find the connection string node by name we have parsed.
Set the value of that node that connection string value.
Save the document (web.COnfig) Again.
Note: It is not the good way to change the web config like this. one can provide other secure way.

How to bind the ASP.NET drop down list DataTextField property to a nested property

I want to bind the DataTextField property of a ASP.NET drop down control to a property of an object that is a property of the initial data source. How would I accomplish that particular task.
Drop down data source data schema
public class A
{
public string ID { get; set; }
public B { get; set; }
}
public class B
{
public string Name { get; set; } //want to bind the DataTextField to this property
}
ASP.NET code behind
DropDownList MyDropDownList = new DropDownList();
List<A> MyList = GetList();
MyDropDownList.DataSource = MyList;
MyDropDownList.DataValueField = "ID";
Say you have a List of A, and want A.ID to be the ID field, and A.B.Name to be the Name field, you cannot bind to B.Name directly, so you either have to create a new property on A to pull the name out of the B property of A or you can use Linq to create an anonymous type that does it for you like this:
List<A> ListA = new List<A>{
new A{ID="1",Item = new B{Name="Val1"}},
new A{ID="2", Item = new B{Name="Val2"}} ,
new A{ID="3", Item = new B{Name="Val3"}}};
DropDownList1.DataTextField = "Name";
DropDownList1.DataValueField = "ID";
DropDownList1.DataSource = from a in ListA
select new { ID, Name = a.Item.Name };
cmb_category.DataSource = cc.getCat(); //source for database
cmb_category.DataTextField = "category_name";
cmb_category.DataValueField = "category_name";
cmb_category.DataBind();
Here's 2 examples for binding a dropdown in ASP.net from a class
Your aspx page
<asp:DropDownList ID="DropDownListJour1" runat="server">
</asp:DropDownList>
<br />
<asp:DropDownList ID="DropDownListJour2" runat="server">
</asp:DropDownList>
Your aspx.cs page
protected void Page_Load(object sender, EventArgs e)
{
//Exemple with value different same as text (dropdown)
DropDownListJour1.DataSource = jour.ListSameValueText();
DropDownListJour1.DataBind();
//Exemple with value different of text (dropdown)
DropDownListJour2.DataSource = jour.ListDifferentValueText();
DropDownListJour2.DataValueField = "Key";
DropDownListJour2.DataTextField = "Value";
DropDownListJour2.DataBind();
}
Your jour.cs class (jour.cs)
public class jour
{
public static string[] ListSameValueText()
{
string[] myarray = {"a","b","c","d","e"} ;
return myarray;
}
public static Dictionary<int, string> ListDifferentValueText()
{
var joursem2 = new Dictionary<int, string>();
joursem2.Add(1, "Lundi");
joursem2.Add(2, "Mardi");
joursem2.Add(3, "Mercredi");
joursem2.Add(4, "Jeudi");
joursem2.Add(5, "Vendredi");
return joursem2;
}
}
You are missing the all important DataBind line!
MyDropDownList.DataBind();

How to sort numerically, instead of by string, on a Telerik RadGrid ASP.NET Ajax?

I have a RadGrid that has a column, Loss Amount, which I would like to be able to sort. Currently it does sort, but only by string value. I have the column type as GridNumericColumn. How can I do it?
My code is listed below. I am also formatting the column text to show the currency. One thing I did notice is that I am returning a string value from the Format method. Could this be the reason?
Column:
<rad:GridNumericColumn DataField="Loss Amount"
UniqueName="Loss Amount"
HeaderText="Loss Amount"
SortExpression="Loss Amount" \>
<HeaderStyle Width="140">
<ItemStyle Width="140" HorizontalAlign="Right"ForeColor="Maroon" />
</rad:GridNumericColumn>`
NeedDataSource Event:
protected void grdCustomerAssignments_NeedDataSource(object source, Telerik.Web.UI.GridNeedDataSourceEventArgs e)
{
GetAssignments();
grdCustomerAssignments.DataSource = Assignments;
}
private void GetAssignments()
{
if (Assignments == null)
Assignments = new DataTable();
SPList list = SPContext.Current.Web.Lists["Listings"];
SPView view = GetCustomerView();
SPListItemCollection items = list.GetItems(view);
Assignments = items.GetDataTable();
foreach (DataColumn column in Assignments.Columns)
{
column.ColumnName = XmlConvert.DecodeName(column.ColumnName);
}
Assignments.AcceptChanges();
}
ItemDataBound Event:
protected void grdCustomerAssignments_ItemDataBound(object sender, GridItemEventArgs e)
{
FormatCurrency(item["Loss Amount"].Text);
}
protected string FormatCurrency(string text)
{
if (text.Length > 3)
{
string result = String.Empty;
char[] tmpArray = text.ToCharArray();
Array.Reverse(tmpArray);
string tmpString = new String(tmpArray);
while (tmpString.Length > 3)
{
string threeChars = tmpString.Substring(0, 3);
result = String.Concat(result, threeChars, ",");
tmpString = tmpString.Remove(0, 3);
}
result = String.Concat(result, tmpString);
tmpArray = result.ToCharArray();
Array.Reverse(tmpArray);
text = new String(tmpArray);
}
return String.Concat("$ ", text);
}
Also check that the source field is of a numeric data type, and it is not string. You can test that easily enabling editing - if you edit an item, an exception will be thrown if a string value is attempted to be assigned to the Telerik numeric editor.

How to Sort on a GridView using ObjectDataSource with TemplateFields

Background:
I am working with a GridView and an ObjectDataSource. I am implementing Paging and Sorting.
On the ObjectDataSource:
objectDataSource.TypeName = value;
objectDataSource.SelectMethod = "Select";
objectDataSource.SelectCountMethod = "SelectCount";
objectDataSource.SortParameterName = "sortExpression";
objectDataSource.EnablePaging = true;
On the GridView:
gridView.AllowPaging = true;
gridView.AllowSorting = true;
gridView.DataSource = objectDataSource;
To get paging and sorting to work, I set "EnableSortingAndPagingCallbacks" to True. Before, I was getting a "System.Web.HttpException: The GridView fired event Sorting which wasn't handled." and this fixes it.
If I use only BoundFields in my GridView, this is great and works fine.
However, if I used TemplateFields, I get a "NotSupportedException: Callbacks are not supported on TemplateField because some controls cannot update properly in a callback. Turn callbacks off on GridView."
Which, makes sense. I just need to know how to make sorting work, without using EnableSortingAndPagingCallbacks.
If EnableSortingAndPagingCallbacks = True:
Paging Works
Sorting Works
BoundFields Work
TemplateFields do Not Work
If EnableSortingAndPagingCallbacks = False:
Paging Works
Sorting does Not Work
BoundFields Work
TemplateFields Work
My Question:
How do I go about getting Paging, Sorting, and TemplateFields to work, all at the same time?
Clarification on the implementation:
Using an ObjectDataSource with a GridView requires implementing a method called Select that provides a sort expression, the number of rows to return, and the start row:
public IEnumerable<CountyAndStateGridRow> Select(string sortExpression, int maximumRows, int startRowIndex)
{
string oql = "select County order by {" + sortExpression + "}" ;
var counties = QueryProvider.ExecuteQuery(oql).Cast<County>();
var page = counties.Skip(startRowIndex).Take(maximumRows);
var rows = page.Select(
county => new CountyAndStateGridRow
{
CountyName = county.Name,
StateName = county.State.Name,
});
return rows;
}
The specific SortExpression is defined in the aspx/ascx:
<Columns>
<asp:BoundField HeaderText="County Name" DataField="CountyName" SortExpression="Name" />
<asp:BoundField HeaderText="State Name" DataField="StateName" SortExpression="State.Name" />
</Columns>
This is supposed to be passed in and call the Select method on the ObjectDataSource when the column is clicked, but it does not seem to work if EnableSortingAndPagingCallbacks = true, and instead I get the exception about the Sorting event not being defined.
For sorting functionality to work:
<asp:GridView GridView ID="GvCountryDetails" AllowPaging="True"
OnPageIndexChanging="GvCountryDetails_PageIndexChanging" AllowSorting="True"
onsorting="GvCountryDetails_Sorting">
in .cs file you need to write
protected void GvCountryDetails_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
GvCountryDetails.PageIndex = e.NewPageIndex;
isPageIndexChanged = true;
BindData();
}
protected void GvCountryDetails_Sorting(object sender, GridViewSortEventArgs e)
{
sortExpression = e.SortExpression;
isPageIndexChanged = false;
BindData();
}
private void SortGridData()
{
string sSortdir;
if (isPageIndexChanged == true)
{
sSortdir = ViewState["SortDirection"] as string;
}
else
{
sSortdir = GetSortDirection(sortExpression);
}
string sSortExp = sortExpression;
if (sSortdir == "ASC")
{
lstCountryDetails = Sort<Country>(lstCountryDetails, sSortExp, SortDirection.Ascending);
}
else
{
lstCountryDetails = Sort<Country>(lstCountryDetails, sSortExp, SortDirection.Descending);
}
}
private List<CountryBO> Sort<TKey>(List<CountryBO> list, string sortBy, SortDirection direction)
{
PropertyInfo property = list.GetType().GetGenericArguments()[0].GetProperty(sortBy);
if (direction == SortDirection.Ascending)
{
return list.OrderBy(e => property.GetValue(e, null)).ToList<CountryBO>();
}
else
{
return list.OrderByDescending(e => property.GetValue(e, null)).ToList<Country>();
}
}
private string GetSortDirection(string column)
{
string sortDirection = "ASC";
string sortExpression = ViewState["SortExpression"] as string;
if (sortExpression != null)
{
if (sortExpression == column)
{
string lastDirection = ViewState["SortDirection"] as string;
if ((lastDirection != null) && (lastDirection == "ASC"))
{
sortDirection = "DESC";
}
}
}
ViewState["SortDirection"] = sortDirection;
ViewState["SortExpression"] = column;
return sortDirection;
}
The property EnableSortingAndPagingCallbacks tells the control to do a client side sort of the data, so that the control appears to automatically sort without a page postback. TemplateFields are not supported with this method. In order to use TemplateFields and perform sorting, you need to wire up the GridView.Sorting event, and set the AllowSorting property to true. Once that is done, the event should fire when the column header is clicked and the sorting logic can be handled from there.
Change the SortExpression value with the value of DataField.
Set AllowPaging and AllowSorting to true.
Set EnableSortingAndPagingCallbacks to true.

ListItems attributes in a DropDownList are lost on postback?

A coworker showed me this:
He has a DropDownList and a button on a web page. Here's the code behind:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ListItem item = new ListItem("1");
item.Attributes.Add("title", "A");
ListItem item2 = new ListItem("2");
item2.Attributes.Add("title", "B");
DropDownList1.Items.AddRange(new[] {item, item2});
string s = DropDownList1.Items[0].Attributes["title"];
}
}
protected void Button1_Click(object sender, EventArgs e)
{
DropDownList1.Visible = !DropDownList1.Visible;
}
On the page load, the items' tooltips are showing, but on the first postback, the attributes are lost. Why is this the case, and are there any workarounds?
I had the same problem and wanted to contribute this resource where the author created an inherited ListItem Consumer to persist attributes to ViewState. Hopefully it will save someone the time I wasted until I stumbled on it.
protected override object SaveViewState()
{
// create object array for Item count + 1
object[] allStates = new object[this.Items.Count + 1];
// the +1 is to hold the base info
object baseState = base.SaveViewState();
allStates[0] = baseState;
Int32 i = 1;
// now loop through and save each Style attribute for the List
foreach (ListItem li in this.Items)
{
Int32 j = 0;
string[][] attributes = new string[li.Attributes.Count][];
foreach (string attribute in li.Attributes.Keys)
{
attributes[j++] = new string[] {attribute, li.Attributes[attribute]};
}
allStates[i++] = attributes;
}
return allStates;
}
protected override void LoadViewState(object savedState)
{
if (savedState != null)
{
object[] myState = (object[])savedState;
// restore base first
if (myState[0] != null)
base.LoadViewState(myState[0]);
Int32 i = 1;
foreach (ListItem li in this.Items)
{
// loop through and restore each style attribute
foreach (string[] attribute in (string[][])myState[i++])
{
li.Attributes[attribute[0]] = attribute[1];
}
}
}
}
Thanks, Laramie. Just what I was looking for. It keeps the attributes perfectly.
To expand, below is a class file I created using Laramie's code to create a dropdownlist in VS2008. Create the class in the App_Code folder. After you create the class, use this line on the aspx page to register it:
<%# Register TagPrefix="aspNewControls" Namespace="NewControls"%>
You can then put the control on your webform with this
<aspNewControls:NewDropDownList ID="ddlWhatever" runat="server">
</aspNewControls:NewDropDownList>
Ok, here's the class...
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Permissions;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace NewControls
{
[DefaultProperty("Text")]
[ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")]
public class NewDropDownList : DropDownList
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
protected override object SaveViewState()
{
// create object array for Item count + 1
object[] allStates = new object[this.Items.Count + 1];
// the +1 is to hold the base info
object baseState = base.SaveViewState();
allStates[0] = baseState;
Int32 i = 1;
// now loop through and save each Style attribute for the List
foreach (ListItem li in this.Items)
{
Int32 j = 0;
string[][] attributes = new string[li.Attributes.Count][];
foreach (string attribute in li.Attributes.Keys)
{
attributes[j++] = new string[] { attribute, li.Attributes[attribute] };
}
allStates[i++] = attributes;
}
return allStates;
}
protected override void LoadViewState(object savedState)
{
if (savedState != null)
{
object[] myState = (object[])savedState;
// restore base first
if (myState[0] != null)
base.LoadViewState(myState[0]);
Int32 i = 1;
foreach (ListItem li in this.Items)
{
// loop through and restore each style attribute
foreach (string[] attribute in (string[][])myState[i++])
{
li.Attributes[attribute[0]] = attribute[1];
}
}
}
}
}
}
Simple solution is to add the tooltip attributes in the pre-render event of the dropdown. Any changes to the state should be done at pre-render event.
sample code :
protected void drpBrand_PreRender(object sender, EventArgs e)
{
foreach (ListItem _listItem in drpBrand.Items)
{
_listItem.Attributes.Add("title", _listItem.Text);
}
drpBrand.Attributes.Add("onmouseover", "this.title=this.options[this.selectedIndex].title");
}
If you only want to load the listitems on the first load of the page then you will need to enable ViewState so that the control can serialize its state there and reload it when the page posts back.
There are several places where ViewState can be enabled - check the <pages/> node in the web.config and also in the <%# page %> directive at the top of the aspx file itself for the EnableViewState property. This setting will need to be true for ViewState to work.
If you don't want to use ViewState, simply remove the if (!IsPostBack) { ... } from around the code that adds the ListItems and the items will be recreated on each postback.
Edit: I apologize - I misread your question. You are correct that the attributes do no survive postback as they are not serialized in ViewState. You must re-add those attributes on each postback.
One simple solution- Call your drop down loading function on the click event where you request for post back.
Here's the VB.Net code of the solution proposed by Laramie and refined by gleapman.
Update: The code I posted below is actually for the ListBox control. Just change the inheritance to DropDownList and rename the class.
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Security.Permissions
Imports System.Linq
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Namespace CustomControls
<DefaultProperty("Text")> _
<ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")>
Public Class PersistentListBox
Inherits ListBox
<Bindable(True)> _
<Category("Appearance")> _
<DefaultValue("")> _
<Localizable(True)> _
Protected Overrides Function SaveViewState() As Object
' Create object array for Item count + 1
Dim allStates As Object() = New Object(Me.Items.Count + 1) {}
' The +1 is to hold the base info
Dim baseState As Object = MyBase.SaveViewState()
allStates(0) = baseState
Dim i As Int32 = 1
' Now loop through and save each attribute for the List
For Each li As ListItem In Me.Items
Dim j As Int32 = 0
Dim attributes As String()() = New String(li.Attributes.Count - 1)() {}
For Each attribute As String In li.Attributes.Keys
attributes(j) = New String() {attribute, li.Attributes(attribute)}
j += 1
Next
allStates(i) = attributes
i += 1
Next
Return allStates
End Function
Protected Overrides Sub LoadViewState(savedState As Object)
If savedState IsNot Nothing Then
Dim myState As Object() = DirectCast(savedState, Object())
' Restore base first
If myState(0) IsNot Nothing Then
MyBase.LoadViewState(myState(0))
End If
Dim i As Int32 = 0
For Each li As ListItem In Me.Items
' Loop through and restore each attribute
' NOTE: Ignore the first item as that is the base state and is represented by a Triplet struct
i += 1
For Each attribute As String() In DirectCast(myState(i), String()())
li.Attributes(attribute(0)) = attribute(1)
Next
Next
End If
End Sub
End Class
End Namespace
Typical solutions to this problem involves creating new controls that are not quite feasible in normal circumstances. There is a simple yet trivial solution to this problem.
The issue is that the ListItem loses its attributes on postback. However, the List itself never loses any custom attributes. One can take advantage of this in a simple yet effective manner thus.
Steps:
Serialize your attributes using the code in the answer above (https://stackoverflow.com/a/3099755/3624833)
Store it to a custom attribute of the ListControl (dropdownlist, checklistbox, whatever).
On post back, read back the custom attribute from the ListControl and then deserialize it back as attributes.
Here is the code I used to (de)serialize attributes (What I needed to do was to keep track of what items of the list were originally rendered as selected when retrieved from the backend and then save or delete rows as per the changes made by the user on the UI):
string[] selections = new string[Users.Items.Count];
for(int i = 0; i < Users.Items.Count; i++)
{
selections[i] = string.Format("{0};{1}", Users.Items[i].Value, Users.Items[i].Selected);
}
Users.Attributes["data-item-previous-states"] = string.Join("|", selections);
(above, "Users" is a CheckboxList control).
On post back (in my case a Submit button Click event), I use the below code to retrieve the same and store them into a Dictionary for post processing:
Dictionary<Guid, bool> previousStates = new Dictionary<Guid, bool>();
string[] state = Users.Attributes["data-item-previous-states"].Split(new char[] {'|'}, StringSplitOptions.RemoveEmptyEntries);
foreach(string obj in state)
{
string[] kv = obj.Split(new char[] { ';' }, StringSplitOptions.None);
previousStates.Add(kv[0], kv[1]);
}
(PS: I have a library funcs that perform error handling and data conversions, omitting the same here for brevity).
Simple solution without ViewState, creating new server control or smth complex:
Creating:
public void AddItemList(DropDownList list, string text, string value, string group = null, string type = null)
{
var item = new ListItem(text, value);
if (!string.IsNullOrEmpty(group))
{
if (string.IsNullOrEmpty(type)) type = "group";
item.Attributes["data-" + type] = group;
}
list.Items.Add(item);
}
Updating:
public void ChangeItemList(DropDownList list, string eq, string group = null, string type = null)
{
var listItem = list.Items.Cast<ListItem>().First(item => item.Value == eq);
if (!string.IsNullOrEmpty(group))
{
if (string.IsNullOrEmpty(type)) type = "group";
listItem.Attributes["data-" + type] = group;
}
}
Example:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
using (var context = new WOContext())
{
context.Report_Types.ToList().ForEach(types => AddItemList(DropDownList1, types.Name, types.ID.ToString(), types.ReportBaseTypes.Name));
DropDownList1.DataBind();
}
}
else
{
using (var context = new WOContext())
{
context.Report_Types.ToList().ForEach(types => ChangeItemList(DropDownList1, types.ID.ToString(), types.ReportBaseTypes.Name));
}
}
}
#Sujay
You could add a semi-colon separated text into the dropdown's value attribute (like csv style), and use String.Split(';') to get 2 "values" out of the one value, as a workaround to get away with not having to create anew user control. Especially if you only have few extra attributes, and if it is not too long. You could also use a JSON value into the dropdown's value attribute and then parse out whatever you need from there.
//In the same block where the ddl is loaded (assuming the dataview is retrieved whether postback or not), search for the listitem and re-apply the attribute
if(IsPostBack)
foreach (DataRow dr in dvFacility.Table.Rows)
{
//search the listitem
ListItem li = ddl_FacilityFilter.Items.FindByValue(dr["FACILITY_CD"].ToString());
if (li!=null)
{
li.Attributes.Add("Title", dr["Facility_Description"].ToString());
}
} //end for each
I managed to achieve that using Session Variables, in my case my list is not going to contain many elements so it works pretty well, this is how I did it:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
string[] elems;//Array with values to add to the list
for (int q = 0; q < elems.Length; q++)
{
ListItem li = new ListItem() { Value = "text", Text = "text" };
li.Attributes["data-image"] = elems[q];
myList.Items.Add(li);
HttpContext.Current.Session.Add("attr" + q, elems[q]);
}
}
else
{
for (int o = 0; o < webmenu.Items.Count; o++)
{
myList.Items[o].Attributes["data-image"] = HttpContext.Current.Session["attr" + o].ToString();
}
}
}
When the Page is loaded first time the list is populated and I add an Image attribute which is lost after postback :( so at the time I add the elements with its attributes I create one Session variable "attr" plus the number of the element taken from the "for" cycle (it will be like attr0, attr1, attr2, etc...) and in them I save the value of the attribute (a path to an image in my case), when postback occurs (inside the "else") I just loop the list and add the attribute taken from the Session variable using the "int" of the "for" loop that is the same as when the page was loaded (this is because in this page I do not add elements to the list just selecting so they have always the same index) and the attributes are set again, I hope this helps someone in the future, greetings!

Resources