I have 2 cascading drop downs, 1 is a parent and the other/child populates once you select a value in the first/parent drop down.
The first one populates on page load and I can set the selected value for this one based off database value that was saved on previous page.
For the second cascading drop down it populates for me when I set the selected value for the parent ID on page load (based off the database value).
But since the second/child drop down is not set/loaded with values until the page loads I can not set the selected value (I tried on Page_Load_Complete but it does not work there either).
I need to know how once I set the selected value in the parent drop down (this works fine), to populate the second drop down based off the value in the first one.
Here is my code for the aspx page. I can set the first selected value, and it populates the second select box (but I can not set the selected value because it is not populated at the time I set the first selected value.
aspx page
<asp:Label ID="lblAffPartCat" Text="<%$ Resources:share,lblAffPartCat %>" runat="server"></asp:Label>
<asp:DropDownList ID="ddlPartCat" runat="server"></asp:DropDownList>
<ajaxToolkit:CascadingDropDown ID="CascadingDropDown2" runat="server" TargetControlID="ddlPartCat"
Category="BasePart" PromptText="<%$ Resources:share,lblSelPartCat %>" LoadingText="[Loading Part Cat...]"
ServicePath="PAIntExtPart.asmx" ServiceMethod="BindPartCat"
ContextKey="" UseContextKey="True"/>
<asp:Label ID="lblAffBasePart" Text="<%$ Resources:share,lblAffBasePart %>" runat="server"></asp:Label>
<asp:DropDownList ID="ddlBasePart" runat="server" ></asp:DropDownList>
<ajaxToolkit:CascadingDropDown ID="ddlBasePart_CascadingDropDown" runat="server" Category="BasePart"
TargetControlID="ddlBasePart" ParentControlID= "ddlPartCat" PromptText="<%$ Resources:share,lblSelBasePart %>"
LoadingText="Loading Base Parts.."
ServicePath="PAIntExtPart.asmx"
ServiceMethod="BindBasePart"
ContextKey="" UseContextKey="True" />
asmx.cs page that populates the drop downs:
using System;
using System.Collections.Generic;
using System.Web.Services;
using System.Data;
using System.Collections.Specialized;
using AjaxControlToolkit;
using Hotline.DataAccess;
/// <summary>
/// Summary description for PAIntExtPart
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
[System.Web.Script.Services.ScriptService()]
public class PAIntExtPart : System.Web.Services.WebService
{
string _SiteLocation = MiscFunctions.getCurrentSiteLocation();
/// <summary>
/// WebMethod to Populate Part Category Dropdown
/// </summary>
[WebMethod]
public CascadingDropDownNameValue[] BindPartCat(string knownCategoryValues, string category, string contextKey)
{
DataTable dsPartCat = null;
// string passed for contextKey is FormType and Language split by ":"
string[] arrcontextKey = contextKey.Split(':');
string FormType = arrcontextKey[0].ToString();
int LanguageID = Int32.Parse(arrcontextKey[1].ToString());
string PartCatValue = arrcontextKey[2].ToString();
try
{
dsPartCat = HarDB.getPartCat(_SiteLocation, LanguageID, FormType);
//create list and add items in it by looping through dataset table
List<CascadingDropDownNameValue> PartCatdetails = new List<CascadingDropDownNameValue>();
foreach (DataRow dtrow in dsPartCat.Rows)
{
string PartCatID = dtrow["PartCatID"].ToString();
string PartCat = dtrow["PartCat"].ToString();
PartCatdetails.Add(new CascadingDropDownNameValue(PartCat, PartCatID));
}
if (PartCatValue.Trim() != "")
{
//SelectedValue = PartCatValue;
}
return PartCatdetails.ToArray();
}
catch (Exception ex)
{
Server.Transfer("Errorpage.aspx?function=getAttachInfo+Error=" + Server.UrlEncode(ex.Message));
return null;
}
}
/// <summary>
/// WebMethod to Populate Base Part Dropdown
/// </summary>
[WebMethod]
public CascadingDropDownNameValue[] BindBasePart(string knownCategoryValues, string category, string contextKey)
{
string PartCatID;
//int LanguageID = Int32.Parse(contextKey);
string[] arrcontextKey = contextKey.Split(':');
string FormType = arrcontextKey[0].ToString();
int LanguageID = Int32.Parse(arrcontextKey[1].ToString());
string BasePartValue = arrcontextKey[2].ToString();
//This method will return a StringDictionary containing the name/value pairs of the currently selected values
StringDictionary PartCatdetails = AjaxControlToolkit.CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);
PartCatID = PartCatdetails["BasePart"];
DataTable dsBasePart = null;
try
{
dsBasePart = HarDB.getBasePart(_SiteLocation, LanguageID, PartCatID, FormType);
//create list and add items in it by looping through dataset table
List<CascadingDropDownNameValue> BasePartdetails = new List<CascadingDropDownNameValue>();
foreach (DataRow dtrow in dsBasePart.Rows)
{
string BasePartID = dtrow["BasePartNumID"].ToString();
string BasePart = dtrow["BasePartNum"].ToString();
BasePartdetails.Add(new CascadingDropDownNameValue(BasePart, BasePartID));
}
if (BasePartValue.Trim() != "")
{
//SelectedValue = PartCatValue;
}
return BasePartdetails.ToArray();
}
catch (Exception ex)
{
Server.Transfer("Errorpage.aspx?function=getAttachInfo+Error=" + Server.UrlEncode(ex.Message));
return null;
}
}
}
I found out my issue, I was not using the correct "Selected Value" when trying to populate the second drop down.
Related
So I have a situation in which I am receiving a collection of Dictionary(string, string) entries where the key of each entry is the column name & value the, well, value. I want to push these to a RadDataGrid so that each dictionary maps to a row. If I knew what/how many columns I'd be getting in advance, I'd just map them to an object and have done with it. Unfortunately, it could be different every time, so that won't work.
So far I'm having no luck. I've tried mapping it*(the collection) directly, converting it to dynamic objects & XMLDocument, none of which worked. Also just got the Fall Creators Update & tried mapping it to a DataTable, no luck there either.
I've been experimenting with mapping the DataTable's DefaultView to the grid's ItemsSource after manually adding columns, but while I get the right # of columns and headers, I still don't get the field values. Not sure where to go next.
Mind you, I'm not married to Telerik. If someone else knows a suitably usable UWP data grid solution that will let me map arbitrary data like this, I'd love to hear about it.
Example using a standard UWP app:
MainPage.xaml:
<Page xmlns:my="using:Telerik.UI.Xaml.Controls.Grid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TestTelerikDataGrid"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ComponentModel="using:System.ComponentModel"
x:Class="TestTelerikDataGrid.MainPage"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="0,0,0,125">
<my:RadDataGrid Margin="0,0,0,-125" x:Name="dataGrid" AutoGenerateColumns="False" >
</my:RadDataGrid>
</Grid>
</Page>
And the back-end:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using Windows.UI.Xaml.Controls;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace TestTelerikDataGrid {
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page {
private ObservableCollection<Dictionary<string, string>> items = new ObservableCollection<Dictionary<string, string>>();
public ObservableCollection<Dictionary<string, string>> ItemDictionary {
get {
return items;
}
set {
items = value;
}
}
public DataTable Items { get; set; }
public MainPage() {
this.InitializeComponent();
CreateItems(); // creates sample data structurally identical to what we'll get in the actual app (i.e., obsv. collection of dictionaries)
CreateTable(); // attempt to take the collection created above and map it to the RadDataGrid
}
private void CreateItems() {
for (int i = 0; i < 5; i++) {
Dictionary<string, string> row = new Dictionary<string, string>();
row["A"] = "A" + i.ToString();
row["B"] = "B" + i.ToString();
row["C"] = "C" + i.ToString();
ItemDictionary.Add(row);
}
}
private void CreateTable() {
Items = new DataTable();
if (ItemDictionary.Count == 0) return;
foreach (KeyValuePair<string, string> entry in ItemDictionary[0]) {
DataColumn column = new DataColumn(entry.Key);
Items.Columns.Add(column);
Telerik.UI.Xaml.Controls.Grid.DataGridTextColumn dgc = new Telerik.UI.Xaml.Controls.Grid.DataGridTextColumn();
dgc.Name = entry.Key;
dgc.Header = entry.Key;
dgc.PropertyName = entry.Key;
dataGrid.Columns.Add(dgc);
}
foreach (Dictionary<string, string> rowEntry in ItemDictionary) {
DataRow row = Items.NewRow();
int col = 0;
foreach (KeyValuePair<string, string> entry in rowEntry) {
row[entry.Key] = entry.Value;
}
Items.Rows.Add(row);
}
DataView dv = Items.DefaultView;
dataGrid.ItemsSource = dv;
}
}
}
Ideally, this will result in a table with 5 rows, 3 columns (A, B, C) and the fields showing the correct value (e.g., first row reading A0, B0, C0).
I've been experimenting with mapping the DataTable's DefaultView to the grid's ItemsSource after manually adding columns, but while I get the right # of columns and headers, I still don't get the field values. Not sure where to go next.
Those are all part of UWP 2.0 that came with the Fall Creators Update. I believe it's build 16299 or something like that. Sorry, should have mentioned that's the build I'm using.
The RaDataGrid doesn't support setting DataTable or DataView as ItemsSource directly. You need to cast to an IEnumerble collection instead of DataView.
Please see this thread on Telerik forum for more details: binding-dictionary-to-raddatagrid
In the end, I wound up creating a generic "GridRow" class with properties "Item00"..."Item99" and just mapped the data to that instead. It's a pill but it works. Just putting this here for the next person.
public class GridRow {
public Int32 Index { get; set; }
public string Item00 { get; set; }
public string Item01 { get; set; }
public string Item02 { get; set; }
...etc
}
public ObservableCollection<GridRow> GridData { get; set; }
And here's how you populate that:
GridData = new ObservableCollection<GridRow>();
foreach (Dictionary<string, string> record in ViewModel.ItemsSource) {
GridRow gridRow = new GridRow();
gridRow.Index = rowIndex;
colIndex = 0;
foreach (DataGridHeader header in ViewModel.Headers) {
gridRow.GetType().GetProperty(string.Format("Item{0:D2}", colIndex)).SetValue(gridRow, record[header.Name]);
colIndex += 1;
}
GridData.Add(gridRow);
rowIndex += 1;
}
You get the idea.
I am having some trouble getting CascadingDropDown lists to work, its displaying the Method 500 error inside the list. I have tried all the common solutions to this and still no luck.
I was originally getting the following error:
System.ArgumentException: Invalid
method name 'getcategories',
method names are case sensitive. The
method name 'GetCategories'
with the same name but different
casing was found. Parameter name:
methodName
Which is odd because I am definately setting the method name in the correct case but it was sending it in lowercase (even though chrome showed the page as sending it in the correct case). Anyhow I worked around this by changing the method name itself to lowercase. This now brings up a new error:
System.InvalidOperationException:
Missing parameter:
knownCategoryValues.
If anyone could shed any light on either of these problems that would be a great help, I've spent way too long on this problem.
Thanks.
UPDATED Code:
<ajaxToolkit:ToolkitScriptManager EnablePageMethods="true" ID="ToolkitScriptManager1" runat="server">
</ajaxToolkit:ToolkitScriptManager>
<ajaxToolkit:CascadingDropDown
ID="CascadingDropDown1"
runat="server"
TargetControlID="mmCategory"
Category="Category"
PromptText="Select a category"
ServicePath="~/DropDownLists.asmx"
ServiceMethod="GetCategories" />
<ajaxToolkit:CascadingDropDown
ID="CascadingDropDown2"
runat="server"
TargetControlID="mmTemplate"
ParentControlID="mmCategory"
PromptText="Select a template"
ServiceMethod="GetTemplates"
ServicePath="~/DropDownLists.asmx"
Category="Template" />
Category: <asp:DropDownList ID="mmCategory" runat="server"/><br/>
Template: <asp:DropDownList ID="mmTemplate" runat="server"/><br/>
Web Service File:
using System;
using System.Web;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Web.Services;
using System.Web.Services.Protocols;
using AjaxControlToolkit;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Linq;
/// <summary>
/// Summary description for TemplateData
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService()]
public class DropDownLists : System.Web.Services.WebService
{
public DropDownLists()
{
//Uncomment the following line if using designed components
//InitializeComponent();
}
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod]
public CascadingDropDownNameValue[] getcategories(string knownCategoryValues, string category)
{
List<CascadingDropDownNameValue> values = new List<CascadingDropDownNameValue>();
values.Add(new CascadingDropDownNameValue("test 1", "1"));
values.Add(new CascadingDropDownNameValue("test 2", "2"));
return values.ToArray();
/*using (MiscDataContext dc = new MiscDataContext())
{
var CatQuery = from D in dc.Templates
select new { D.Category }
into CatQueryResults
select CatQueryResults;
foreach (var CatResult in CatQuery)
{
string CatReturn = CatResult.Category;
values.Add(new CascadingDropDownNameValue(CatReturn, CatReturn));
}
return values.ToArray();
}*/
}
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod]
public CascadingDropDownNameValue[] GetTemplates(string knownCategoryValues, string category)
{
StringDictionary kv = CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);
string varCat;
varCat = kv["Category"];
List<CascadingDropDownNameValue> values = new List<CascadingDropDownNameValue>();
values.Add(new CascadingDropDownNameValue("test 3", "3"));
values.Add(new CascadingDropDownNameValue("test 4", "4"));
return values.ToArray();
/*using (MiscDataContext dc = new MiscDataContext())
{
var CatQuery = from D in dc.Templates
where(D.Category == varCat)
select new { D.ID, D.Name }
into CatResult
select CatResult;
foreach (var CatResult in CatQuery)
{
int ID = (int)CatResult.ID;
string Name = CatResult.Name;
values.Add(new CascadingDropDownNameValue(Name, Convert.ToString(ID)));
}
return values.ToArray();
}*/
}
}
In order to be usable with a CascadingDropDown, your web method's prototype must be:
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod]
public CascadingDropDownNameValue[] GetCategories(
string knownCategoryValues, string category)
{
}
If you assign something to the ContextKey property of the extender, that prototype becomes:
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod]
public CascadingDropDownNameValue[] GetCategories(
string knownCategoryValues, string category, string contextKey)
{
}
The knownCategoryValues parameter represents the current selection in the master dropdown. You should pass it to CascadingDropDown.ParseKnownCategoryValuesString() to obtain its actual value.
I currently have a string that I want to limit to 200 characters.
I don't know how to format it so if it's less, it wont change, but if its more, it will trim it.
This is in a ListView Control, NOT a Repeater. Sorry for that, my mistake.
<ItemTemplate>
<div class="portfolio_title">
<div class="custom_title">
<%# DataBinder.Eval(Container.DataItem, "Title")%></div>
</div>
<asp:Literal ID="LiteralArticle" runat="server"></asp:Literal>
<%# DataBinder.Eval(Container.DataItem, "Article")%><br />
Read Full Article...
<div class="page_line">
</div>
</ItemTemplate>
Here is some code that I use for this sort of thing. Attach this to the OnRowDataBound event. This truncates to 50 characters and adds elipses "...".
protected void CommentGridViewRowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
TableCell cell = e.Row.Cells[0];
if (!string.IsNullOrEmpty(cell.Text) && cell.Text.Length > 50)
{
cell.Text = cell.Text.Substring(0, 50) + "…";
}
}
}
Do you mean like...
int maxLength = 200;
string trimmed = (trimmed.length > maxLength) ? trimmed.Substring(0,maxlength) : trimmed ;
I assume this is going in a grid or something... I'd call a function and pass your Eval as an argument:
My example:
<asp:Image ID="imgTopLevelTickCross" runat="server" ImageUrl='<%# "/images/" & getImage(Eval("DrwgID").toString()) & ".gif" %> ' />
The ImageURL calls getImage and passes the value of Eval("DrwgID") to it to form the src path
Public Function getImage(ByVal drwgID As Integer) As String
If TopLevelDrwgID = drwgID Then
Return "True"
Else
Return "blank"
End If
End Function
It's maybe a litte more than you need, but works well for me in most cases. It preserves the file ending, if you deal with files and adds "..." at the end of the shortend string if you want.
/// <summary>
/// Shortens a long string. Optionally keeps the file ending and adds a placeholder at the end.
/// </summary>
/// <example>
/// Input: ThisIsAVeryLongFilenameForThisTest.doc (length=10, placeholder='...', saveFileEnding=true)
/// Output: ThisIsAVeryLong
/// </example>
/// <param name="value"></param>
/// <param name="length"></param>
/// <param name="placeHolder"></param>
/// <param name="saveFileEnding"></param>
/// <returns></returns>
public static string ShowSummary(string value, int length, string placeHolder, bool saveFileEnding)
{
int lengthNew = length;
string fileEnding = "";
//nothing to do if the string is short enough
if (length > value.Length)
{
return value;
}
if (saveFileEnding)
{
int index = value.LastIndexOf(".");
if (index != -1)
{
fileEnding = value.Substring(index);
lengthNew = length - fileEnding.Length;
}
}
//substract the length of the placeholder
lengthNew = lengthNew - placeHolder.Length;
if (lengthNew > 0)
{
return value.Substring(0, lengthNew) + placeHolder + fileEnding;
}
else
{
//something is weird, maybe a really long filending or a '.' in the filename, so just cut it down
return value.Substring(0, length);
}
}//ShowSummary
I am getting [MethodError 500] when I use cascading drop down. below is my code
<tr>
<td >
Select a Hoster:
</td>
<td>
<asp:DropDownList ID="ddlFeaturedHoster" runat="server" ></asp:DropDownList>
</td>
</tr>
<ajaxToolkit:CascadingDropDown ID="cddHoster" runat="server" TargetControlID="ddlFeaturedHoster"
PromptText="Select a Hoster" LoadingText="Loading ..." Category="ActiveHoster"
ServiceMethod="GetDropDownContents" ServicePath="~/Hosting/HostingService.asmx"/>
Service Code:
[WebMethod]
[ScriptMethod]
public CascadingDropDownNameValue[] GetActiveHosters()
{
List<CascadingDropDownNameValue> returnList = new List<CascadingDropDownNameValue>();
HostersManager hosterManager = new HostersManager();
List<Hosters_HostingProviderDetail> hosters = hosterManager.GetAllHosters();
returnList.Add(new CascadingDropDownNameValue("--Please Select One--","0",true));
foreach (Hosters_HostingProviderDetail item in hosters)
{
returnList.Add(new CascadingDropDownNameValue() { name=item.HostingProviderName, value= item.HosterID.ToString()});
}
return returnList.ToArray() ;
}
[WebMethod]
[ScriptMethod]
public CascadingDropDownNameValue[] GetDropDownContents(string knownCategoryValues, string category)
{
knownCategoryValues = FormatCategoryWord(knownCategoryValues);
List<CascadingDropDownNameValue> values = new List<CascadingDropDownNameValue>();
HostersManager hosterManager = new HostersManager();
switch (category)
{
case "ActiveHoster":
values.AddRange(GetActiveHosters());
break;
case "ActiveOffer":
values.AddRange(GetActiveOffers(1));
break;
}
return values.ToArray<CascadingDropDownNameValue>();
}
/// <summary>
/// Formats the category word
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private string FormatCategoryWord(string value)
{
if (string.IsNullOrEmpty(value)) return value;
if (value.LastIndexOf(":") > 0) value = value.Substring(value.LastIndexOf(":") + 1);
if (value.LastIndexOf(";") > 0) value = value.Substring(0, value.LastIndexOf(";"));
return value;
}
}
How about some try catch blocks within your webMethods with some exception logging?
I kept [ScriptService] attribute on top of service class its working now.
I have an ASP.NET Webforms site that is regularly having features added.
The majority of time a new WebControl is added to the page and I need to increment the TabIndex to all subsequent controls on the page.
I'd prefer a more robust solution than choosing an arbitrary gap between the initial assigned tab indexes. Setting the tab indexes using the designer tab order functionality is one option but I'd prefer to stay in the source view.
Ideally, if I had, for example, three check boxes I'd like to be able to define the tabindex based off the previous controls tabindex. Then I'd only need to insert the new control and change one existing control.
For example, add a new property TabIndexAfterControlId to WebControl:
<asp:CheckBox ID="checkBoxA" runat="server" TabIndex="1"/>
<asp:CheckBox ID="checkBoxB" runat="server" TabIndexAfterControlId="checkBoxA"/>
<asp:CheckBox ID="checkBoxC" runat="server" TabIndexAfterControlId="checkBoxB"/>
My first thought was to extend System.Web.UI.WebControls.WebControl with a new property, but extension properties aren't supported.
Note: This approach worked for some webcontrols (DropDownLists) but not all of them (CheckBoxes). I've left it here for reference.
I've ended up with a solution that uses a code behind method to capture the relationship between controls.
<asp:CheckBox ID="checkBoxA" runat="server" TabIndex="1"/>
<asp:CheckBox ID="checkBoxB" runat="server" TabIndex='<%# TabIndexAfter(checkBoxB, checkBoxA) %>'/>
<asp:CheckBox ID="checkBoxC" runat="server" TabIndex='<%# TabIndexAfter(checkBoxC, checkBoxB) %>'/>
The code behind method will initially do a basic TabIndex assignment, which works well when the tab order follows the order of the controls on a page. Then during the PreRender event the tab index order will be checked again. This is important if the tab order doesn't not follow the natural flow of the page.
private LinkedList<WebControl> _webControlTabOrder;
/// <summary>
/// Assign the current WebControl TabIndex a value greater than the prior WebControl.
/// </summary>
/// <param name="currentWebControl">The current WebControl to set the TabIndex for</param>
/// <param name="priorWebControl">The prior WebControl to get the previous TabIndex from.</param>
/// <returns>The new TabIndex for the control</returns>
public int TabIndexAfter(WebControl currentWebControl, WebControl priorWebControl)
{
if (_webControlTabOrder == null)
{
_webControlTabOrder = new LinkedList<WebControl>();
this.PreRender += new EventHandler(UserControlBase_PreRender);
}
LinkedListNode<WebControl> priorNode = _webControlTabOrder.Find(currentWebControl);
if (priorNode == null)
{
priorNode = _webControlTabOrder.AddLast(priorWebControl);
}
_webControlTabOrder.AddAfter(priorNode, currentWebControl);
return priorWebControl.TabIndex + 1;
}
void UserControlBase_PreRender(object sender, EventArgs e)
{
LinkedListNode<WebControl> currentNode = _webControlTabOrder.First;
while(currentNode.Next != null)
{
LinkedListNode<WebControl> nextNode = currentNode.Next;
if (nextNode.Value.TabIndex <= currentNode.Value.TabIndex)
{
nextNode.Value.TabIndex = (short)(currentNode.Value.TabIndex + 1);
}
currentNode = nextNode;
}
}
My prior attempt using the data-binding syntax (<%# ... %>) to set the TabIndex for web controls failed when some controls wouldn't bind the TabIndex (CheckBox). It also wasn't ideal as I needed to pass a reference to the current control into the code behind method.
This time around I went with a custom ExpressionBuilder that accepts the name of the web control that the current control should follow in the tab order.
The TabIndexAfterExpressionBuilder initially returns the short -1 as the value. At the same time it registers with the LoadComplete event of the current Page. When this event fires both controls are found and the tab indexes set according to their relative positions.
Example WebControls using the TabIndex Expression Builder
<asp:TextBox ID="txtTextBox0" runat="server" TabIndex="1" /><br />
<asp:TextBox ID="txtTextBox1" runat="server" TabIndex="<%$ TabIndex:txtTextBox0 %>" /><br />
<asp:TextBox ID="txtTextBox2" runat="server" TabIndex="<%$ TabIndex:txtTextBox1 %>" />
TabIndexExpressionBuilder.cs
namespace ExpressionBuilders
{
public class TabIndexExpressionBuilder : ExpressionBuilder
{
public override System.CodeDom.CodeExpression GetCodeExpression(System.Web.UI.BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
string priorControlId = entry.Expression.Trim();
string currentControlId = entry.ControlID;
CodeExpression[] inputParams = new CodeExpression[] { new CodePrimitiveExpression(priorControlId),
new CodePrimitiveExpression(currentControlId),
new CodeTypeOfExpression(entry.DeclaringType),
new CodePrimitiveExpression(entry.PropertyInfo.Name) };
// Return a CodeMethodInvokeExpression that will invoke the GetRequestedValue method using the specified input parameters
return new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(this.GetType()),
"GetRequestedValue",
inputParams);
}
public static object GetRequestedValue(string priorControlId, string currentControlId, Type targetType, string propertyName)
{
if (HttpContext.Current == null)
{
return null;
}
Page page = HttpContext.Current.Handler as Page;
if (page != null)
{
page.LoadComplete += delegate(object sender, EventArgs e)
{
WebControl currentWebControl = FindControlRecursive(page, currentControlId);
WebControl priorWebControl = FindControlRecursive(page, priorControlId);
if (currentWebControl != null && priorWebControl != null)
{
TabIndexAfter(page, currentWebControl, priorWebControl);
}
};
}
// Default TabIndex
short value = (short)-1;
return value;
}
private static WebControl FindControlRecursive(Control rootControl, string controlID)
{
if (rootControl.ID == controlID) { return rootControl as WebControl; }
foreach (Control controlToSearch in rootControl.Controls)
{
Control controlToReturn = FindControlRecursive(controlToSearch, controlID);
if (controlToReturn != null)
{
return controlToReturn as WebControl;
}
}
return null;
}
#region Tabbing
/// <summary>
/// Assign the current WebControl TabIndex a value greater than the prior WebControl.
/// </summary>
/// <param name="currentWebControl">The current Control to set the TabIndex for</param>
/// <param name="priorWebControl">The prior Control to get the previous TabIndex from.</param>
/// <returns>The new TabIndex for the current control</returns>
private static short TabIndexAfter(Page page, WebControl currentWebControl, object prior)
{
TabOrderWebControl tabOrderWebControl = page.FindControl("TabOrderWebControl") as TabOrderWebControl;
if (tabOrderWebControl == null)
{
tabOrderWebControl = new TabOrderWebControl();
page.Controls.Add(tabOrderWebControl);
}
WebControl priorWebControl = prior as WebControl;
if (priorWebControl == null)
{
string priorWebControlId = prior as string;
priorWebControl = page.FindControl(priorWebControlId) as WebControl;
}
if (currentWebControl == null) { throw new ArgumentNullException("currentWebControl"); }
if (priorWebControl == null) { throw new ArgumentNullException("priorWebControl"); }
if (currentWebControl == priorWebControl) { throw new ArgumentException("priorWebControl is the same as the currentWebControl", "priorWebControl"); }
tabOrderWebControl.TabIndexAfter(currentWebControl, priorWebControl);
return currentWebControl.TabIndex;
}
#endregion
}
}
TabOrderWebControl.cs
namespace ExpressionBuilders
{
public class TabOrderWebControl :
WebControl
{
LinkedList<WebControl> _webControlTabOrder;
internal void TabIndexAfter(System.Web.UI.WebControls.WebControl currentWebControl, System.Web.UI.WebControls.WebControl priorWebControl)
{
if (_webControlTabOrder == null)
{
_webControlTabOrder = new LinkedList<WebControl>();
this.Page.PreRender += new EventHandler(PageBase_PreRender);
}
LinkedListNode<WebControl> priorNode = _webControlTabOrder.Find(priorWebControl);
LinkedListNode<WebControl> currentNode = _webControlTabOrder.Find(currentWebControl);
if (currentNode != null)
{
//The current node is already in the list (it must preceed some other control)
//Add the prior node before it.
if (priorNode == null)
{
priorNode = _webControlTabOrder.AddBefore(currentNode, priorWebControl);
}
else
{
//Both nodes are already in the list. Ensure the ordering is correct.
bool foundPriorNode = false;
foreach (WebControl controlNode in _webControlTabOrder)
{
if (controlNode == priorWebControl)
{
foundPriorNode = true;
}
else if (controlNode == currentWebControl)
{
if (foundPriorNode)
{
//Ordering is correct
break;
}
else
{
throw new ApplicationException(string.Format("WebControl ordering is incorrect. Found {1} before {0}", currentWebControl.ID, priorWebControl.ID));
}
}
}
}
}
else if (priorNode == null)
{
//Neither control is in the list yet.
priorNode = _webControlTabOrder.AddLast(priorWebControl);
currentNode = _webControlTabOrder.AddAfter(priorNode, currentWebControl);
}
else
{
//Prior node is already in the list but the current node isn't
currentNode = _webControlTabOrder.AddAfter(priorNode, currentWebControl);
}
}
/// <summary>
/// Once all the controls have been added to the linked list ensure the tab ordering is correct.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void PageBase_PreRender(object sender, EventArgs e)
{
AssignTabIndexes();
}
/// <summary>
/// Reassign tab indexes for all known controls.
/// </summary>
protected void AssignTabIndexes()
{
LinkedListNode<WebControl> currentNode = _webControlTabOrder.First;
while (currentNode.Next != null)
{
LinkedListNode<WebControl> nextNode = currentNode.Next;
WebControl currentControl = currentNode.Value;
WebControl nextControl = nextNode.Value;
if (currentControl == nextControl)
{
throw new ApplicationException("Control added twice");
}
short currentTabIndex = currentControl.TabIndex;
short nextTabIndex = nextControl.TabIndex;
if (nextTabIndex <= currentTabIndex)
{
nextControl.TabIndex = (short)(currentTabIndex + 1);
}
currentNode = nextNode;
}
}
}
}
web.config
<system.web>
<compilation debug="true" targetFramework="4.0">
<expressionBuilders>
<add expressionPrefix="TabIndex" type="ExpressionBuilders.TabIndexExpressionBuilder, ExpressionBuilders"/>
</expressionBuilders>
</compilation>
</system.web>