Setting a WebControls TabIndex based on the ControlId of another control - asp.net

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>

Related

Asp.Net Repeater loses data on postback

My problem is a bit more complicated than the title says:
I made a user control (I called it editor) for editing Data base data into another user control I also made(I called it GridView).
The editor is use for each row (a row is also a usercontrol and the editor is only inside row even for insert) inside my gridview and work perfectly but not when I try to use it to insert.
The only difference between insert and edit is this Field:
#region Field
/// <summary>
///
/// </summary>
//public Field Field { get { return DataItem as Field; } }
private Field _field;
[Bindable(true)]
public Field Field
{
get
{
if (IsInsert && _field == null)
{
_field = SubscriptionController.CreateField();
}
return _field;
}
set { _field = value; }
}
#endregion
Inside this Field I've the collection I bind to the repeater
the SubscriptionController.CreateField(); method just create an instance of Field class and all collection inside here is the code:
public Field CreateField()
{
Field field = new Field();
field.Type = GetFieldTypes().First();
field.Label = new LocalizedStringCollection();
field.Values = new FieldValueCollection();
field.Selections = new FieldSelectionCollection();
foreach (Models.TrainingGroup trainingGroup in GetTrainingGroup())
{
foreach (Models.Division division in GetDivisions())
{
foreach (Models.ProfilStatusGroup profilStatusGroup in GetProfilStatusGroup())
{
field.Selections.Add(new Models.FieldSelection() { Selected = false, DivisionId = division.Id, ProfilStatusGroupId = profilStatusGroup.Id, TrainingGroupId = trainingGroup.Id });
}
}
}
}
the collection I bind is stored in viewstate :
#region FieldValues
/// <summary>
/// Get/Set FieldValues from Viewstate
/// </summary>
public FieldValueCollection FieldValues
{
get
{
if (ViewState["FieldValues"] == null)
{
if (Field != null && Field.Values != null)
ViewState.Add("FieldValues", Field.Values);
else
ViewState.Add("FieldValues", new FieldValueCollection());
}
if (ViewState["FieldValues"] != null)
{
return (FieldValueCollection)ViewState["FieldValues"];
}
return null;
}
set
{
if (ViewState["FieldValues"] == null)
{
ViewState.Add("FieldValues", value);
}
else
{
ViewState["FieldValues"] = value;
}
}
}
#endregion
but when I get on postback all textboxes inside my repeater are empty.

ASP.NET custom controls - custom property doesn't hold the assigned value on postaback

I have a custom asp-net control that inherits from another one and its works as expected, though the properties are only set properly if i code them in the markup directly, so for instance if i need set a property at runtime that is some dynamic value, this value is never set or somehow lost.
Here's the markup code:
<!--related form-->
<fw:advancedformdisplay id="formDisp" runat="server" captchaenabled="true" EmailEnabled="true" EnableViewState="true" captchaprivatekey="xxxxxxxxxxxxxxxxxxxx" captchapublickey="xxxxxxxxxxxxx" captchatheme="white" SourceType="MenuItem" SourceMainId="Auto">
</fw:advancedformdisplay>
This is the code of the control:
[DefaultProperty("CaptchaEnabled"),ToolboxData("<{0}:AdvancedFormDisplay runat=server></{0}:AdvancedFormDisplay>"), Description("This is an enhanced FormDisplay control that inlcudes Googles Captcha control is enabled")]
public class AdvancedFormDisplay :SiteBuilder.WebControls.FormDisplay
{
bool _CaptchaEnabled = false, sendEmail = false;
string captchaErrorMessage = "The verification code entered is not valid. Please try again!";
RecaptchaControl captchaControl = null;
string captchaPrivateKey = "", captchaPublicKey = "", captchaTheme = "clean";
string originalFormHtml = string.Empty;
string afterText = string.Empty, beforeText = string.Empty;
Literal litHtmlForm = null;
string captchaErrorClass = "errorCaptcha";
public string EmailBeforeText
{
get { return beforeText; }
set { beforeText = value; }
}
public string EmailAfterText
{
get { return afterText; }
set { afterText = value; }
}
public string CaptchaErrorClass
{
get { return captchaErrorClass; }
set { captchaErrorClass = value; }
}
public bool CaptchaEnabled
{
get { return _CaptchaEnabled; }
set { _CaptchaEnabled = value; }
}
public bool EmailEnabled
{
get { return sendEmail; }
set { sendEmail = value; }
}
public string CaptchaErrorMessage
{
get { return captchaErrorMessage; }
set { captchaErrorMessage = value; }
}
/// <summary>
/// red,white,blackglass,clean
/// </summary>
public string CaptchaTheme
{
get { return captchaTheme; }
set { captchaTheme = value; }
}
public string CaptchaPrivateKey
{
get { return captchaPrivateKey; }
set { captchaPrivateKey = value; }
}
public string CaptchaPublicKey
{
get { return captchaPublicKey; }
set { captchaPublicKey = value; }
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
}
public override void OnSaved(FormDisplayEventArgs e)
{
//If captcha control is enabled we need to adda bit of code to redirect form properly
if (CaptchaEnabled && e.Redirect && !e.SendMail)
{
//Do Stuff
}
if(sendEmail)
{
//Send email
}
base.OnSaved(e);
}
public override void OnSaving(FormDisplayEventArgs e)
{
if (CaptchaEnabled)
{
//Validate and do stuff
}
base.OnSaving(e);
}
}
And then in my asp.net page that is using control, created by markup code, in the Page_Load() i try to assign some values to some properties and and the values aren't set properly, meaning that if i have set for isntance, the property EmailBeforeText = "somthing" this value will not be assigned..
protected void Page_Load(object sender, EventArgs e)
{
//2: Get the language of menuitem - Based on current culture setting (for by dropdownbox - change logic)
try
{
currentCulture = Thread.CurrentThread.CurrentCulture.ToString();
// Redirect if domain does not match rootnode.
DomainChecker.CheckURL(this.Request, this.Response, currentCulture);
if (footerArticle != null)
footerArticle.SourceMenuId = Digimaker.Config.Custom.Get("FooterID_" + currentCulture).ToString();
}
catch
{
currentCulture = "en-GB";
if( footerArticle != null )
footerArticle.SourceMenuId = Digimaker.Config.Custom.Get("FooterID_" + currentCulture).ToString();
}
Any ideas what i'm missing here?
Thanks a lot for your reading!
Regards,
byte_slave
short answer: use viewstate to persist your custom values!
Understanding ASP.NET ViewState whitepaper (see example with NavigateUrl)
edit: as reading the white-paper is obviously a really hard thing:
Each control is responsible for storing its own state, which is
accomplished by adding its changed state to its ViewState property.
The ViewState property is defined in the System.Web.UI.Control class,
meaning that all ASP.NET server controls have this property available.
(When talking about view state in general I'll use lower case letters
with a space between view and state; when discussing the ViewState
property, I'll use the correct casing and code-formatted text.)
If you examine the simple properties of any ASP.NET server control
you'll see that the properties read and write directly to the view
state. (You can view the decompiled source code for a .NET assembly by
using a tool like Reflector.) For example, consider the HyperLink Web
control's NavigateUrl property. The code for this property looks like
so:
public string NavigateUrl
{
get
{
string text = (string) ViewState["NavigateUrl"];
if (text != null)
return text;
else
return string.Empty;
}
set
{
ViewState["NavigateUrl"] = value;
}
}
As this code sample illustrates, whenever a control's property is
read, the control's ViewState is consulted. If there is not an entry
in the ViewState, then the default value for the property is returned.
When the property is assigned, the assigned value is written directly
to the ViewState.

FindControl() method for Dynamically Nested Controls on PostBack

How do you get a specific nested control of dynamically created controls (i.e. the child of a dynamic control)? The FindControl() method does not work because it only deals with TopLevel dynamic controls I believe.
You need to recurse through your controls: (C# code)
public static Control FindControl(Control parentControl, string fieldName)
{
if (parentControl != null && parentControl.HasControls())
{
Control c = parentControl.FindControl(fieldName);
if (c != null)
{
return c;
}
// if arrived here, then not found on this level, so search deeper
// loop through collection
foreach (Control ctrl in parentControl.Controls)
{
// any child controls?
if (ctrl.HasControls())
{
// try and find there
Control c2 = FindControl(ctrl, fieldName);
if (c2 != null)
{
return c2; // found it!
}
}
}
}
return null; // found nothing (in this branch)
}
This is an extension method I've used in times past. I've found that using it as an extension method makes the code a little more expressive, but that's just preference.
/// <summary>
/// Extension method that will recursively search the control's children for a control with the given ID.
/// </summary>
/// <param name="parent">The control who's children should be searched</param>
/// <param name="controlID">The ID of the control to find</param>
/// <returns></returns>
public static Control FindControlRecursive(this Control parent, string controlID)
{
if (!String.IsNullOrEmpty(parent.ClientID) && parent.ClientID.Equals(controlID)) return parent;
System.Web.UI.Control control = null;
foreach (System.Web.UI.Control c in parent.Controls)
{
control = c.FindControlRecursive(controlID);
if (control != null)
break;
}
return control;
}

How to use ASP.Net server controls inside of Substitution control?

while the method we use in Substitution control should return strings, so how is it possible to use a donut caching in web forms on a server control which should be rendered server side?
for example Loginview control?
UPDATE
This is now a fully working example. There a few things happening here:
Use the call back of a substitution control to render the output of the usercontrol you need.
Use a custom page class that overrides the VerifyRenderingInServerForm and EnableEventValidation to load the control in order to prevent errors from being thrown when the usercontrol contains server controls that require a form tag or event validation.
Here's the markup:
<asp:Substitution runat="server" methodname="GetCustomersByCountry" />
Here's the callback
public string GetCustomersByCountry(string country)
{
CustomerCollection customers = DataContext.GetCustomersByCountry(country);
if (customers.Count > 0)
//RenderView returns the rendered HTML in the context of the callback
return ViewManager.RenderView("customers.ascx", customers);
else
return ViewManager.RenderView("nocustomersfound.ascx");
}
Here's the helper class to render the user control
public class ViewManager
{
private class PageForRenderingUserControl : Page
{
public override void VerifyRenderingInServerForm(Control control)
{ /* Do nothing */ }
public override bool EnableEventValidation
{
get { return false; }
set { /* Do nothing */}
}
}
public static string RenderView(string path, object data)
{
PageForRenderingUserControl pageHolder = new PageForUserControlRendering();
UserControl viewControl = (UserControl) pageHolder.LoadControl(path);
if (data != null)
{
Type viewControlType = viewControl.GetType();
FieldInfo field = viewControlType.GetField("Data");
if (field != null)
{
field.SetValue(viewControl, data);
}
else
{
throw new Exception("ViewFile: " + path + "has no data property");
}
}
pageHolder.Controls.Add(viewControl);
StringWriter result = new StringWriter();
HttpContext.Current.Server.Execute(pageHolder, result, false);
return result.ToString();
}
}
See these related questions:
Turn off page-level caching in a
user control
UserControl’s RenderControl is
asking for a form tag in (C#
.NET)
One thing Micah's answer left out is that the substitution function must be static, accept a HttpContext parameter, and return a string. See this msdn page for more info.
I've also extended Micah's helper class to be a little more flexible.
Markup
<asp:Substitution ID="Substitution1" MethodName="myFunction" runat="server" />
Implemenation
public static string myFunction(HttpContext httpContext){
ViewManager vm = new ViewManager();
//example using a Button control
Button b = new Button();
b.Text = "click me"; //we can set properties like this
//we can also set properties with a Dictionary Collection
Dictionary<string,object> data = new Dictionary<string,object>();
data.add("Visible",true);
String s = vm.RenderView(b,data); //don't do anything (just for example)
//we can also use this class for UserControls
UserControl myControl = vm.GetUserControl("~mypath");
data.clear();
data.add("myProp","some value");
return vm.RenderView(myControl,data); //return for Substitution control
}
Class
using System.IO;
using System.ComponentModel;
public class ViewManager
{
private PageForRenderingUserControl pageHolder;
public ViewManager()
{
pageHolder = new PageForRenderingUserControl();
}
public UserControl GetUserControl(string path)
{
return (UserControl)pageHolder.LoadControl(path);
}
public string RenderView(Control viewControl, Dictionary<string, object> data)
{
pageHolder.Controls.Clear();
//Dim viewControl As UserControl = DirectCast(pageHolder.LoadControl(Path), UserControl)
if (data != null) {
Type viewControlType = viewControl.GetType();
dynamic properties = TypeDescriptor.GetProperties(viewControl);
foreach (string x in data.Keys) {
if ((properties.Item(x) != null)) {
properties.Item(x).SetValue(viewControl, data[x]);
}
}
}
pageHolder.Controls.Add(viewControl);
StringWriter result = new StringWriter();
HttpContext.Current.Server.Execute(pageHolder, result, false);
return result.ToString();
}
private class PageForRenderingUserControl : Page
{
public override void VerifyRenderingInServerForm(Control control)
{
// Do nothing
}
public override bool EnableEventValidation {
get { return false; }
// Do nothing
set { }
}
}
}
Thanks again to Micah for the code
I'm fairly certain you can't do this - the Substitution control will only allow you to insert a string into an outputcached page.
This makes sense if you think about the whole output of a server control, which could be a <table> that'll disrupt all your carefully crafted markup and/or something that requires a load of <script> injected into the page - whereas injecting a single string is something that's relatively straightforward.

How to validate against Multiple validation groups?

I have two validation groups: parent and child
I have an add button that needs to only validate the child validation group which is easily done. The save button needs to validate against the parent and child validation groups, both client side and server side. I think I know how to do it server side by calling the Page.Validate("groupname") method for each group, but how can it be done client side?
You should be able to accomplish this by creating a javascript function that uses Page_ClientValidate and then having the button call that function
<asp:Button ID="btnSave" Text="Save" OnClientClick="return validate()" runat="server" />
<script type="text/javascript">
function validate() {
var t1 = Page_ClientValidate("parent");
var t2 = Page_ClientValidate("child");
if (!t1 || !t2) return false;
return true;
}
</script>
The problem with CAbbott's answer is that validation errors that occur in the "parent" group will not be displayed after the call to validate the "child" group. The more minor problem with Oleg's answer is that validation of the "child" group will not occur until the "parent" group is ready.
All we really need to do to allow client-side validation of more than one group at the same time is to override the Javascript IsValidationGroupMatch method which determines whether or not a control is to be included in the current set being validated.
For example:
(function replaceValidationGroupMatch() {
// If this is true, IsValidationGroupMatch doesn't exist - oddness is afoot!
if (!IsValidationGroupMatch) throw "WHAT? IsValidationGroupmatch not found!";
// Replace ASP.net's IsValidationGroupMatch method with our own...
IsValidationGroupMatch = function(control, validationGroup) {
if (!validationGroup) return true;
var controlGroup = '';
if (typeof(control.validationGroup) === 'string') controlGroup = control.validationGroup;
// Deal with potential multiple space-delimited groups being validated
var validatingGroups = validationGroup.split(' ');
for (var i = 0; i < validatingGroups.length; i++) {
if (validatingGroups[i] === controlGroup) return true;
}
// Control's group not in any being validated, return false
return false;
};
} ());
// You can now validate against multiple groups at once, for example:
// space-delimited list. This would validate against the Decline group:
//
// Page_ClientValidate('Decline');
//
// while this would validate against the Decline, Open and Complete groups:
//
// Page_ClientValidate('Open Decline Complete');
//
// so if you wanted to validate all three upon click of a button, you'd do:
<asp:Button ID="yourButton" runat="server"
OnClick="ButtonSave_Click" CausesValidation="false"
OnClientClick="return Page_ClientValidate('Open Decline Complete');" />
If you call Page_ClientValidate(..) twice, only the last validation result will be shown and it can be OK while the first is not. So the second call should be made only if the first has returned true
<script type="text/javascript">
var parentOk= Page_ClientValidate('parent');
var childOk = false;
if (parentOk) {
childOk = Page_ClientValidate('child');
}
return parentOk && childOk;
</script>
Whatever way you do it requires some hacking to get round ASP.Net's assumption that you wouldn't try to do this. I favour a reusable approach which is explicit about the hackery involved.
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace WebSandbox.Validators
{
/// <summary>
/// <para>
/// Validates a different validation group. Among the use cases envisioned are
/// <list type="">
/// <item>
/// Validating one set of rules when the user clicks "Save draft" and validating those rules plus some
/// extra consistency checks when they click "Send".
/// </item>
/// <item>
/// Grouping controls in a <code>fieldset</code> into a validation group with a
/// <code>ValidationSummary</code> and then having a final <code>ValidationSummary</code> which tells the
/// user which groups still have errors.
/// </item>
/// </list>
/// </para>
/// <para>
/// We include checks against setting <code>GroupToValidate</code> to the same value as
/// <code>ValidationGroup</code>, but we don't yet include checks for infinite recursion with one validator
/// in group A which validates group B and another in group B which validates group A. Caveat utilitor.
/// </para>
/// </summary>
public class ValidationGroupValidator : BaseValidator
{
public string GroupToValidate
{
get { return ViewState["G2V"] as string; }
set { ViewState["G2V"] = value; }
}
protected override bool ControlPropertiesValid()
{
if (string.IsNullOrEmpty(GroupToValidate)) throw new HttpException("GroupToValidate not specified");
if (GroupToValidate == ValidationGroup) throw new HttpException("Circular dependency");
// Don't call the base, because we don't want a "control to validate"
return true;
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
base.AddAttributesToRender(writer);
writer.AddAttribute("evaluationfunction", "ValidateValidationGroup");
writer.AddAttribute("GroupToValidate", GroupToValidate);
}
protected override void OnPreRender(EventArgs e)
{
// The standard validation JavaScript is too restrictive for this validator to work, so we have to replace a key function.
// Fortunately this runs later than the standard JS, so we can simply overwrite the existing value of Page_ClientValidate.
Page.ClientScript.RegisterStartupScript(typeof(ValidationGroupValidator), "validationJS", _ValidationJS);
base.OnPreRender(e);
}
protected override bool EvaluateIsValid()
{
if (string.IsNullOrEmpty(GroupToValidate)) return false;
bool groupValid = true;
foreach (IValidator validator in Page.GetValidators(GroupToValidate))
{
validator.Validate();
groupValid &= validator.IsValid;
}
return groupValid;
}
private const string _ValidationJS = #"<script type=""text/javascript"">
function ValidateValidationGroup(val) {
if (typeof(val.GroupToValidate) == ""string"") {
val.valid = PageMod_DoValidation(val.GroupToValidate);
}
}
function Page_ClientValidate(validationGroup) {
Page_InvalidControlToBeFocused = null;
if (!Page_Validators) return true;
var i, ctrl;
// Mark everything as valid.
for (i = 0; i < Page_Validators.length; i++) {
Page_Validators[i].finalValid = true;
}
if (Page_ValidationSummaries) {
for (i = 0; i < Page_ValidationSummaries.length; i++) {
Page_ValidationSummaries[i].finalDisplay = ""none"";
}
}
// Validate.
var groupValid = PageMod_DoValidation(validationGroup);
// Update displays once.
for (i = 0; i < Page_Validators.length; i++) {
ctrl = Page_Validators[i];
ctrl.isvalid = ctrl.finalValid;
ValidatorUpdateDisplay(ctrl);
}
if (Page_ValidationSummaries) {
for (i = 0; i < Page_ValidationSummaries.length; i++) {
ctrl = Page_ValidationSummaries[i];
ctrl.style.display = ctrl.finalDisplay;
}
}
ValidatorUpdateIsValid();
Page_BlockSubmit = !Page_IsValid;
return Page_IsValid;
}
function PageMod_DoValidation(validationGroup) {
var groupValid = true, validator, i;
for (i = 0; i < Page_Validators.length; i++) {
validator = Page_Validators[i];
ValidatorValidate(validator, validationGroup, null);
validator.finalValid &= validator.isvalid;
groupValid &= validator.isvalid;
}
if (Page_ValidationSummaries) {
ValidationSummaryOnSubmit(validationGroup, groupValid);
var summary;
for (i = 0; i < Page_ValidationSummaries.length; i++) {
summary = Page_ValidationSummaries[i];
if (summary.style.display !== ""none"") summary.finalDisplay = summary.style.display;
}
}
return groupValid;
}
</script>";
}
}

Resources