Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Boosting EXT.NET Development Through Component Encapsulation

Tech May 14 1

Efficient development in any framework often hinges on identifying and abstracting repetitive tasks. In the context of EXT.NET, where rich client-side components interact with server-side logic, encapsulating common UI operations into reusable utility functions can drastically improve productivity and maintainability.

Dynamically Populating Radio Button and Checkbox Groups

A frequent requirement in web applications involves presenting users with a list of options, often including an "Other" choice that prompts for custom input. Manually configuring this dynamic behavior for every form can be cumbersome. To streamline this, we can implement helper methods that abstract the data binding and client-side interaction.

Consider a scenario where a radio group or checkbox group needs to be populated from a data source, with the last option triggering a text input. The following C# helper methods facilitate this by using reflection to bind a generic list of objects to an EXT.NET RadioGroup or CheckboxGroup. They also inject client-side JavaScript to handle the "Other" option's custom input.

The core of the dynamic "Other" functionality relies on a JavaScript function that uses Ext.MessageBox.prompt to gather user input. This function is registered once on the page and then referenced by the last item in the group.

function promptForRemark(hiddenFieldCmp, checkboxCmp, originalLabel, displayLength) {
    // Check if the checkbox is being checked
    if (checkboxCmp.getValue()) {
        Ext.Msg.prompt(
            originalLabel, // Dialog title
            'Please enter ' + originalLabel + ':', // Prompt message
            function (buttonId, enteredText) {
                if (buttonId === 'ok') {
                    const sanitizedText = enteredText.trim();
                    hiddenFieldCmp.setValue(sanitizedText); // Save entered text to hidden field

                    if (sanitizedText) {
                        // Update checkbox label with truncated text if input exists
                        const truncatedDisplay = sanitizedText.length > displayLength ?
                                                 sanitizedText.substring(0, displayLength) + '...' :
                                                 sanitizedText;
                        checkboxCmp.setBoxLabel(originalLabel + ': ' + truncatedDisplay);
                    } else {
                        // Revert to original label if input is empty
                        checkboxCmp.setBoxLabel(originalLabel);
                    }
                } else {
                    // User cancelled, optionally uncheck the checkbox
                    Ext.Msg.alert('Info', 'Operation cancelled.');
                    // If the other option was specifically designed to require input,
                    // one might uncheck it here.
                    // checkboxCmp.setValue(false);
                }
            },
            this, // Scope for the function
            true, // Make input multiline
            hiddenFieldCmp.getValue() // Pre-fill with existing value if any
        );
    }
}

On the server-side, C# helper methods dynamically create and configure the EXT.NET controls:

using Ext.Net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web.UI;
using ListItem = Ext.Net.ListItem; // Alias to avoid conflict with System.Web.UI.WebControls.ListItem

public static class ExtControlBinder
{
    // Helper to throw detailed exceptions for missing properties
    private static void ThrowPropertyNotFoundException(Type type, string propertyName)
    {
        throw new InvalidOperationException($"Property '{propertyName}' not found on type '{type.FullName}'. Ensure property name matches case and spelling.");
    }

    /// <summary>
    /// Binds a generic list of objects to an Ext.Net.RadioGroup,
    /// optionally adding a special "Other" item with custom input functionality.
    /// </summary>
    /// <typeparam name="T">The type of objects in the list.</typeparam>
    /// <param name="page">The System.Web.UI.Page where the control resides.</param>
    /// <param name="dataSource">The list of objects to bind.</param>
    /// <param name="controlId">The ID of the RadioGroup control.</param>
    /// <param name="textField">The name of the property for the item's display text.</param>
    /// <param name="valueField">The name of the property for the item's value.</param>
    /// <param name="checkedValue">The value that should be pre-selected (if `isProperty` is false), or the property name indicating selection (if `isProperty` is true).</param>
    /// <param name="isProperty">True if `checkedValue` is a property name, false if it's a specific value to match.</param>
    /// <param name="columnCount">Number of columns to display the radio buttons.</param>
    /// <param name="otherOptionLabel">Label for the "Other" option. Null if no "Other" option.</param>
    /// <param name="otherOptionDisplayLength">Max length for "Other" text in label.</param>
    public static void BindRadioGroup<T>(
        System.Web.UI.Page page,
        List<T> dataSource,
        string controlId,
        string textField,
        string valueField,
        string checkedValue,
        bool isProperty,
        int? columnCount,
        string otherOptionLabel = null,
        int otherOptionDisplayLength = 0)
    {
        if (dataSource == null || !dataSource.Any()) return;

        Control targetControl = page.FindControl(controlId);
        if (targetControl is RadioGroup extRadioGroup)
        {
            if (!string.IsNullOrEmpty(otherOptionLabel))
            {
                // Register the client-side script for "Other" functionality
                string scriptKey = "PromptForRemarkScript";
                if (!page.ClientScript.IsStartupScriptRegistered(scriptKey))
                {
                    page.ClientScript.RegisterStartupScript(page.GetType(), scriptKey, GetPromptForRemarkScript(), true);
                }
            }

            extRadioGroup.Items.Clear();
            columnCount = columnCount ?? 4; // Default to 4 columns
            extRadioGroup.ColumnsNumber = dataSource.Count <= columnCount ? dataSource.Count : columnCount.Value;

            int itemIndex = 0;
            foreach (var item in dataSource)
            {
                Type itemType = item.GetType();
                PropertyInfo textProp = itemType.GetProperty(textField) ?? ThrowPropertyNotFoundException(itemType, textField);
                PropertyInfo valueProp = itemType.GetProperty(valueField) ?? ThrowPropertyNotFoundException(itemType, valueField);

                Radio radioItem = new Radio
                {
                    ID = $"{controlId}Item{itemIndex}",
                    BoxLabel = textProp.GetValue(item)?.ToString() ?? string.Empty,
                    Tag = valueProp.GetValue(item)?.ToString() ?? string.Empty,
                    InputValue = valueProp.GetValue(item)?.ToString() ?? string.Empty
                };

                if (isProperty)
                {
                    PropertyInfo checkProp = itemType.GetProperty(checkedValue) ?? ThrowPropertyNotFoundException(itemType, checkedValue);
                    radioItem.Checked = (checkProp.GetValue(item)?.ToString() == "1" || (checkProp.GetValue(item)?.ToString()?.ToLower() == "true"));
                }
                else
                {
                    radioItem.Checked = (radioItem.Tag == checkedValue);
                }
                extRadioGroup.Items.Add(radioItem);
                itemIndex++;
            }

            // Apply "Other" functionality to the last item if specified
            if (!string.IsNullOrEmpty(otherOptionLabel) && extRadioGroup.Items.Count > 0)
            {
                Radio lastRadio = extRadioGroup.Items.Last() as Radio;
                if (lastRadio != null)
                {
                    // Ensure a hidden field exists for storing the custom remark
                    // User must add: <ext:Hidden ID="controlIdHidden" runat="server" />
                    lastRadio.Listeners.Check.Handler =
                        $"promptForRemark(#{{{controlId}Hidden}}, this, '{otherOptionLabel}', {otherOptionDisplayLength});";
                }
            }
        }
        else if (targetControl is System.Web.UI.WebControls.RadioButtonList aspRadioList)
        {
            aspRadioList.DataTextField = textField;
            aspRadioList.DataValueField = valueField;
            aspRadioList.DataSource = dataSource;
            aspRadioList.RepeatDirection = System.Web.UI.WebControls.RepeatDirection.Horizontal;
            aspRadioList.DataBind();
            if (!isProperty) // Only for direct value matching
            {
                aspRadioList.SelectedValue = checkedValue;
            }
            // Note: ASP.NET RadioButtonList does not easily support the "Other" dynamic input out of the box.
        }
    }

    /// <summary>
    /// Binds a generic list of objects to an Ext.Net.CheckboxGroup,
    /// optionally adding a special "Other" item with custom input functionality.
    /// </summary>
    /// <typeparam name="T">The type of objects in the list.</typeparam>
    /// <param name="page">The System.Web.UI.Page where the control resides.</param>
    /// <param name="dataSource">The list of objects to bind.</param>
    /// <param name="controlId">The ID of the CheckboxGroup control.</param>
    /// <param name="textField">The name of the property for the item's display text.</param>
    /// <param name="valueField">The name of the property for the item's value.</param>
    /// <param name="checkedField">The name of the boolean/indicator property for item selection.</param>
    /// <param name="columnCount">Number of columns to display the checkboxes.</param>
    /// <param name="otherOptionLabel">Label for the "Other" option. Null if no "Other" option.</param>
    /// <param name="otherOptionDisplayLength">Max length for "Other" text in label.</param>
    public static void BindCheckGroup<T>(
        System.Web.UI.Page page,
        List<T> dataSource,
        string controlId,
        string textField,
        string valueField,
        string checkedField,
        int? columnCount,
        string otherOptionLabel = null,
        int otherOptionDisplayLength = 0)
    {
        if (dataSource == null || !dataSource.Any()) return;

        Control targetControl = page.FindControl(controlId);
        if (targetControl is CheckboxGroup extCheckboxGroup)
        {
            if (!string.IsNullOrEmpty(otherOptionLabel))
            {
                // Register the client-side script for "Other" functionality
                string scriptKey = "PromptForRemarkScript";
                if (!page.ClientScript.IsStartupScriptRegistered(scriptKey))
                {
                    page.ClientScript.RegisterStartupScript(page.GetType(), scriptKey, GetPromptForRemarkScript(), true);
                }
            }

            extCheckboxGroup.Items.Clear();
            columnCount = columnCount ?? 4; // Default to 4 columns
            extCheckboxGroup.ColumnsNumber = dataSource.Count <= columnCount ? dataSource.Count : columnCount.Value;

            int itemIndex = 0;
            foreach (var item in dataSource)
            {
                Type itemType = item.GetType();
                PropertyInfo textProp = itemType.GetProperty(textField) ?? ThrowPropertyNotFoundException(itemType, textField);
                PropertyInfo valueProp = itemType.GetProperty(valueField) ?? ThrowPropertyNotFoundException(itemType, valueField);
                PropertyInfo checkProp = itemType.GetProperty(checkedField) ?? ThrowPropertyNotFoundException(itemType, checkedField);

                Checkbox checkboxItem = new Checkbox
                {
                    ID = $"{controlId}Item{itemIndex}",
                    BoxLabel = textProp.GetValue(item)?.ToString() ?? string.Empty,
                    Tag = valueProp.GetValue(item)?.ToString() ?? string.Empty,
                    InputValue = valueProp.GetValue(item)?.ToString() ?? string.Empty,
                    ToolTip = textProp.GetValue(item)?.ToString() ?? string.Empty // Optional tooltip
                };

                var checkValue = checkProp.GetValue(item)?.ToString();
                checkboxItem.Checked = (checkValue == "1" || (checkValue != null && checkValue.ToLower() == "true"));

                extCheckboxGroup.Items.Add(checkboxItem);
                itemIndex++;
            }

            // Apply "Other" functionality to the last item if specified
            if (!string.IsNullOrEmpty(otherOptionLabel) && extCheckboxGroup.Items.Count > 0)
            {
                Checkbox lastCheckbox = extCheckboxGroup.Items.Last() as Checkbox;
                if (lastCheckbox != null)
                {
                    // Ensure a hidden field exists for storing the custom remark
                    // User must add: <ext:Hidden ID="controlIdHidden" runat="server" />
                    lastCheckbox.Listeners.Check.Handler =
                        $"promptForRemark(#{{{controlId}Hidden}}, this, '{otherOptionLabel}', {otherOptionDisplayLength});";
                }
            }
        }
        else if (targetControl is System.Web.UI.WebControls.CheckBoxList aspCheckboxList)
        {
            aspCheckboxList.RepeatDirection = System.Web.UI.WebControls.RepeatDirection.Horizontal;
            aspCheckboxList.RepeatLayout = System.Web.UI.WebControls.RepeatLayout.Table;
            aspCheckboxList.RepeatColumns = columnCount ?? 7;
            aspCheckboxList.Width = System.Web.UI.WebControls.Unit.Percentage(100);

            foreach (var item in dataSource)
            {
                Type itemType = item.GetType();
                PropertyInfo textProp = itemType.GetProperty(textField) ?? ThrowPropertyNotFoundException(itemType, textField);
                PropertyInfo valueProp = itemType.GetProperty(valueField) ?? ThrowPropertyNotFoundException(itemType, valueField);
                PropertyInfo checkProp = itemType.GetProperty(checkedField) ?? ThrowPropertyNotFoundException(itemType, checkedField);

                System.Web.UI.WebControls.ListItem li = new System.Web.UI.WebControls.ListItem
                {
                    Text = textProp.GetValue(item)?.ToString() ?? string.Empty,
                    Value = valueProp.GetValue(item)?.ToString() ?? string.Empty
                };

                var checkValue = checkProp.GetValue(item)?.ToString();
                li.Selected = (checkValue == "1" || (checkValue != null && checkValue.ToLower() == "true"));
                aspCheckboxList.Items.Add(li);
            }
        }
    }

    private static string GetPromptForRemarkScript()
    {
        return @"
            function promptForRemark(hiddenFieldCmp, checkboxCmp, originalLabel, displayLength) {
                if (checkboxCmp.getValue()) {
                    Ext.Msg.prompt(
                        originalLabel,
                        'Please enter ' + originalLabel + ':',
                        function (buttonId, enteredText) {
                            if (buttonId === 'ok') {
                                const sanitizedText = enteredText.trim();
                                hiddenFieldCmp.setValue(sanitizedText);
                                if (sanitizedText) {
                                    const truncatedDisplay = sanitizedText.length > displayLength ?
                                                             sanitizedText.substring(0, displayLength) + '...' :
                                                             sanitizedText;
                                    checkboxCmp.setBoxLabel(originalLabel + ': ' + truncatedDisplay);
                                } else {
                                    checkboxCmp.setBoxLabel(originalLabel);
                                }
                            } else {
                                Ext.Msg.alert('Info', 'Operation cancelled.');
                            }
                        },
                        this,
                        true,
                        hiddenFieldCmp.getValue()
                    );
                }
            }";
    }
}

To use this, your ASP.NET markup for the form would enclude an Ext.Net.RadioGroup or CheckboxGroup, and crucially, an Ext.Net.Hidden field with a convention-based ID (e.g., if your group ID is myRadioGroup, the hidden field ID would be myRadioGroupHidden) to store the custom input.

Example invocation for a checkbox group with an "Other" option:

// Assuming _dataService is an object that fetches your data
List<CategoryOption> options = _dataService.GetCategoryOptions("TerminationReason").ToList();

ExtControlBinder.BindCheckGroup(
    this.Page, // 'this' refers to the page or user control
    options,
    "cblTerminationReason", // ID of your CheckboxGroup
    "AttributeValue",      // Property for display text
    "AttributeID",         // Property for value
    "IsSelected",          // Property indicating if checked
    4,                     // Number of columns
    "Other Reason",        // Label for the "Other" option
    15                     // Max characters to show in the label
);

Streamlining Dropdown List Population

Binding data to EXT.NET ComboBox controls is another common task. While EXT.NET offers sophisticated data binding with Store components and AJAX proxies, a simple reflection-based approach can be effective for small, static datasets.

/// <summary>
/// Binds a generic list of objects to an Ext.Net.ComboBox using reflection.
/// </summary>
/// <typeparam name="T">The type of objects in the list.</typeparam>
/// <param name="page">The System.Web.UI.Page where the control resides.</param>
/// <param name="dataSource">The list of objects to bind.</param>
/// <param name="controlId">The ID of the ComboBox control.</param>
/// <param name="textField">The name of the property for the item's display text.</param>
/// <param name="valueField">The name of the property for the item's value.</param>
/// <param name="selectedValue">The value to pre-select in the ComboBox.</param>
public static void BindComboBox<T>(
    System.Web.UI.Page page,
    List<T> dataSource,
    string controlId,
    string textField,
    string valueField,
    string selectedValue = null)
{
    if (dataSource == null || !dataSource.Any()) return;

    ComboBox targetComboBox = page.FindControl(controlId) as ComboBox;
    if (targetComboBox == null) return;

    targetComboBox.Items.Clear();
    foreach (var item in dataSource)
    {
        Type itemType = item.GetType();
        PropertyInfo textProp = itemType.GetProperty(textField) ?? ThrowPropertyNotFoundException(itemType, textField);
        PropertyInfo valueProp = itemType.GetProperty(valueField) ?? ThrowPropertyNotFoundException(itemType, valueField);

        ListItem comboBoxItem = new ListItem
        {
            Text = textProp.GetValue(item)?.ToString() ?? string.Empty,
            Value = valueProp.GetValue(item)?.ToString() ?? string.Empty
        };
        targetComboBox.Items.Add(comboBoxItem);
    }

    if (!string.IsNullOrEmpty(selectedValue))
    {
        targetComboBox.SetValue(selectedValue);
    }
}

For larger datasets or scenarios requiring lazy loading and filtering, integrating Ext.Net.Store components with a JsonReader and an HttpProxy remains the recommended appproach in EXT.NET.

Handling SharePoint Privileges in EXT.NET

When deploying EXT.NET applications within a SharePoint environment, elevated privileges are often necessary for certain server-side operations, such as rendering embedded scripts and styles. This ensures that EXT.NET resources are correctly loaded and function as expected.

using Ext.Net;
using Microsoft.SharePoint;
using System.Web.UI;

public static class SharePointExtNetHelpers
{
    /// <summary>
    /// Ensures EXT.NET scripts and styles are built with elevated SharePoint privileges
    /// if the request is not an AJAX request.
    /// </summary>
    /// <param name="resourceManager">The EXT.NET ResourceManager instance.</param>
    public static void ApplyElevatedPrivilegesForExtNetResources(this ResourceManager resourceManager)
    {
        // Only run on initial page load, not during AJAX callbacks
        if (!X.IsAjaxRequest)
        {
            SPSecurity.RunWithElevatedPrivileges(() =>
            {
                // Ensure scripts are embedded and built
                resourceManager.RenderScripts = ResourceLocationType.Embedded;
                resourceManager.BuildScripts();

                // Ensure styles are embedded and built
                resourceManager.RenderStyles = ResourceLocationType.Embedded;
                resourceManager.BuildStyles();
            });
        }
    }
}

This extension method can be called once on your page's ResourceManager instance during the initial page load to ensure proper resource handling in SharePoint.

Bi-directional Data Flow: Object to Controls and Vice-Versa

A common pattern in data-driven forms is populating UI controls from a data model object and then mapping control values back to the object. Reflection offers a flexible way to achieve this, reducing boilerplate code for each form.

using Ext.Net;
using System;
using System.Reflection;
using System.Web.UI; // For Control

public static class ControlValueMapper
{
    // Helper to get the underlying type of a Nullable<T>
    private static Type GetUnderlyingType(Type type)
    {
        return Nullable.GetUnderlyingType(type) ?? type;
    }

    /// <summary>
    /// Populates properties of a data model object from corresponding EXT.NET input controls
    /// within a given container control.
    /// Controls are expected to have IDs prefixed with "txt" (e.g., "txtPropertyName").
    /// Handles DateField, TextFieldBase, and nullable types.
    /// </summary>
    /// <typeparam name="TModel">The type of the data model object.</typeparam>
    /// <param name="containerControl">The container (e.g., Page or UserControl) holding the input controls.</param>
    /// <param name="modelObject">The data model object to populate.</param>
    public static void SetModelPropertiesFromControls<TModel>(this Control containerControl, TModel modelObject)
        where TModel : class
    {
        if (modelObject == null) return;

        Type modelType = modelObject.GetType();
        foreach (PropertyInfo property in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (!property.CanWrite) continue; // Only set writable properties

            Control uiControl = containerControl.FindControl("txt" + property.Name);
            if (uiControl == null) continue;

            string controlValue = string.Empty;
            if (uiControl is DateField dateField)
            {
                if (dateField.IsEmpty)
                {
                    // For nullable DateTime, set to null if control is empty
                    if (GetUnderlyingType(property.PropertyType) == typeof(DateTime))
                    {
                        property.SetValue(modelObject, null, null);
                        continue;
                    }
                    // For non-nullable DateTime, let Convert.ChangeType handle default (e.g., DateTime.MinValue)
                }
                else
                {
                    controlValue = dateField.Text;
                }
            }
            else if (uiControl is TextFieldBase textField)
            {
                controlValue = textField.Text.Trim();
            }
            // Add more control types as needed (e.g., Checkbox, ComboBox)

            if (string.IsNullOrEmpty(controlValue) && GetUnderlyingType(property.PropertyType) != typeof(string))
            {
                // If value is empty and property is not string, try to set null for nullable types
                if (Nullable.GetUnderlyingType(property.PropertyType) != null)
                {
                    property.SetValue(modelObject, null, null);
                }
                // Otherwise, Convert.ChangeType will handle default values for non-nullable types or throw error if conversion fails
                continue;
            }

            try
            {
                Type targetType = GetUnderlyingType(property.PropertyType);
                object convertedValue = Convert.ChangeType(controlValue, targetType);
                property.SetValue(modelObject, convertedValue, null);
            }
            catch (Exception ex)
            {
                // Log or handle conversion errors (e.g., invalid date format, non-numeric input for int property)
                System.Diagnostics.Debug.WriteLine($"Error converting '{controlValue}' to type '{property.PropertyType.Name}' for property '{property.Name}': {ex.Message}");
            }
        }
    }

    /// <summary>
    /// Populates EXT.NET display/input controls within a container control from properties
    /// of a data model object. Controls are expected to have IDs prefixed with "txt".
    /// </summary>
    /// <typeparam name="TModel">The type of the data model object.</typeparam>
    /// <param name="containerControl">The container (e.g., Page or UserControl) holding the controls.</param>
    /// <param name="modelObject">The data model object providing the values.</param>
    public static void SetControlValuesFromModel<TModel>(this Control containerControl, TModel modelObject)
        where TModel : class
    {
        if (modelObject == null) return;

        Type modelType = modelObject.GetType();
        foreach (PropertyInfo property in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (!property.CanRead) continue; // Only read readable properties

            Control uiControl = containerControl.FindControl("txt" + property.Name);
            if (uiControl == null) continue;

            object propertyValue = property.GetValue(modelObject, null);
            string displayValue = propertyValue?.ToString() ?? string.Empty;

            if (uiControl is TextFieldBase textField)
            {
                textField.Text = displayValue;
            }
            else if (uiControl is DisplayField displayField)
            {
                displayField.Text = displayValue;
            }
            // Add more control types as needed (e.g., DateField, ComboBox for value setting)
        }
    }
}

These extension methods simplify the mapping process, particularly for forms with many fields. Note the handling of nullable types in SetModelPropertiesFromControls.

Implementing Generic Client-Side Form Validation

Client-side validation is critical for a responsive user experience. EXT.NET's FormPanel allows injecting custom JavaScript for client-side validation through its ClientValidation.Handler. This can be leveraged to create a generic validation framework that supports custom logic and provides consistent feedback.

First, define a global JavaScript function to display consistent validation notifications:

let lastNotificationCss = ''; // Global variable to prevent identical notifications from stacking

function displayValidationStatus(title, contentHtml, cssClass) {
    // Only show a new notification if the status (CSS class) has changed
    if (lastNotificationCss !== cssClass) {
        lastNotificationCss = cssClass;
        Ext.net.Notification.show({
            hideFx: { fxName: 'switchOff', args: [{}] }, // Custom hide effect
            showFx: { args: ['C3DAF9', 1, { duration: 2.0 }], fxName: 'frame' }, // Custom show effect
            iconCls: cssClass, // CSS class for the notification icon (e.g., 'val-accept', 'val-exclamation')
            closeVisible: true,
            html: contentHtml,
            title: title + '   ' + Ext.Date.format(new Date(), 'g:i:s A') // Add timestamp to title
        });
    }
}

Next, a C# helper method can inject the generic client-side validation handler into any FormPanel:

using Ext.Net;
using System.Web.UI;

public static class FormValidationHelper
{
    /// <summary>
    /// Attaches a generic client-side validation handler to an Ext.Net.FormPanel.
    /// This handler integrates with optional custom validation functions and provides UI feedback.
    /// </summary>
    /// <param name="formPanel">The Ext.Net.FormPanel to attach the handler to.</param>
    public static void ApplyGenericClientValidationHandler(this FormPanel formPanel)
    {
        // Prevent re-attaching if already set
        if (!string.IsNullOrEmpty(formPanel.Listeners.ClientValidation.Handler))
            return;

        formPanel.Listeners.ClientValidation.Handler = @"
            var isValid = valid; // 'valid' is passed by Ext.Net FormPanel itself, representing its internal field validation status
            var validationMessages = [];
            var customValidationResult = { IsValid: true, Message: '' };

            // Check for a custom validation function defined on the page
            if (typeof (performCustomFormValidation) === 'function') {
                customValidationResult = performCustomFormValidation(false, isValid); // Pass current status to custom validator
                if (typeof (customValidationResult.IsValid) !== 'undefined') {
                    isValid = isValid && customValidationResult.IsValid;
                    if (customValidationResult.Message) {
                        validationMessages.push('<span style=\'color:red;\'>' + customValidationResult.Message + '</span>');
                    }
                } else {
                    isValid = isValid && customValidationResult; // Fallback if custom validator returns boolean directly
                }
            }

            // Disable/enable submit buttons based on validation status
            // Use #{ControlID} syntax to reference server controls in client-side code
            if (typeof(#{btnSubmitForm}) !== 'undefined' && #{btnSubmitForm} !== null) {
                #{btnSubmitForm}.setDisabled(!isValid);
            }
            if (typeof(#{btnSaveDraft}) !== 'undefined' && #{btnSaveDraft} !== null) {
                #{btnSaveDraft}.setDisabled(!isValid); // Example for another button
            }

            // Determine overall status message and icon
            var statusIconClass = isValid ? 'val-accept' : 'val-exclamation';
            var statusMessage = validationMessages.length > 0 ? validationMessages.join('<br/>') :
                                isValid ? '<span style=\'color:green;\'>Validation passed. Ready to submit.</span>' :
                                          '<span style=\'color:red;\'>Input errors detected. Please check highlighted fields.</span>';

            // Update form's bottom toolbar status and display notification
            this.getBottomToolbar().setStatus({ text: statusMessage, iconCls: statusIconClass });
            displayValidationStatus('Validation Status', statusMessage, statusIconClass);
        ";
    }
}

This generic handler checks for a custom JavaScript function named performCustomFormValidation(). If found, it incorporates its result into the overall validation status. It also dynamically enables/disables submit buttons (e.g., btnSubmitForm, btnSaveDraft) and updates the form's status bar and a global notification.

Implementing Custom Validation Logic

To add specific validation rules for a form, you define the performCustomFormValidation() function on your ASP.NET page. This function can leverage other helper functions for complex checks, such as summing values from multiple input fields.

    // Example helper for sum validation
    function validateSumMax(fieldIds, maxValue, customErrorMessage) {
        if (!fieldIds || fieldIds.length === 0) {
            return { IsValid: true, Message: '' };
        }

        let currentSum = 0;
        for (let i = 0; i < fieldIds.length; i++) {
            // Use Ext.getCmp to get component by its ID
            const field = Ext.getCmp(fieldIds[i]);
            if (field) {
                const value = field.getValue();
                const numericValue = parseInt(value, 10);
                currentSum += isNaN(numericValue) ? 0 : numericValue;

                if (currentSum > maxValue) {
                    return {
                        IsValid: false,
                        Message: customErrorMessage || (`Current total (${currentSum}) exceeds maximum allowed (${maxValue}).`)
                    };
                }
            }
        }
        return { IsValid: true, Message: '' };
    }

    // Define field IDs for sum validation (use Ext.Net's IDMode.Static for predictable IDs)
    // For demonstration, these IDs are placeholders. In real EXT.NET, use #{{ControlID}} or IDMode.Static.
    const productGroup1FieldIds = ['txtProduct1Count', 'txtProduct2Count', 'txtProduct3Count'];
    const productGroup2FieldIds = ['txtService1Hours', 'txtService2Hours'];

    // The main custom validation entry point called by the generic handler
    function performCustomFormValidation(isInitialCheck, currentOverallValidity) {
        // If the Ext.Net FormPanel already determined client-side errors, respect that.
        if (!currentOverallValidity) return { IsValid: false, Message: '' };

        // Apply specific validation rules
        let sumValidationResult1 = validateSumMax(productGroup1FieldIds, 5, "Total items in Group 1 cannot exceed 5.");
        if (!sumValidationResult1.IsValid) {
            // Optionally show a detailed message box for complex errors
            Ext.MessageBox.show({
                title: 'Validation Error',
                msg: sumValidationResult1.Message,
                buttons: Ext.MessageBox.OK,
                icon: Ext.MessageBox.ERROR
            });
            return sumValidationResult1; // Return immediately on first failure
        }

        let sumValidationResult2 = validateSumMax(productGroup2FieldIds, 10, "Total hours for services cannot exceed 10.");
        if (!sumValidationResult2.IsValid) {
            Ext.MessageBox.show({
                title: 'Validation Error',
                msg: sumValidationResult2.Message,
                buttons: Ext.MessageBox.OK,
                icon: Ext.MessageBox.ERROR
            });
            return sumValidationResult2;
        }

        return { IsValid: true, Message: '' }; // All custom validations passed
    }

This approach provides a robust and extensible validation system where generic form validation logic is separated from specific business rules. It also gracefully handles the presence or absence of custom validation logic and specific UI elements like submit buttons, improving the reusability of the generic handler.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.