Rectangle 27 0

c How to avoid writing messy JavaScript in an ASP.NET code behind?


$(document).on("focusout", "input[type=text][data-capitalize-disabled^=true]", function(event) {
    event.target.value = event.target.value.replace(/(^|\s+)[a-z]/g, function(match, $1) {
        return $1.toUpperCase();
    });
});
<asp:BulletedList ID="ErrorSummary" runat="server" CssClass="Error-Summary" />

<p>
    <asp:Label ID="PostTitleLabel" AssociatedControlID="PostTitle" runat="server">* Title:</asp:Label>
    <asp:TextBox ID="PostTitle" runat="server" />
    <asp:Label ID="PostTitleError" runat="server" CssClass="Error" />
</p>

<p>
    <asp:Label ID="PostBodyLabel" AssociatedControlID="PostBody" runat="server">* Body:</asp:Label>
    <asp:TextBox ID="PostBody" runat="server" TextMode="MultiLine" />
    <asp:Label ID="PostBodyError" runat="server" CssClass="Error" />
</p>

<asp:HiddenField ID="PostId" runat="server" />
<asp:Panel ID="FooPanel" runat="server">
    <button type="button" data-action="fillOptions">Fill Options</button>
    <asp:DropDownList ID="OptionsDropdown" runat="server" />
</asp:Panel>

<script type="text/javascript">
    var foo = new FooWidget("<%# FooPanel.ClientId %>");
</script>
<asp:TextBox ... data-capitalize-disabled="true" />
PostTitle.Attributes["data-capitalize-disabled"] = "true";
PostValidator validator = new PostValidator();

foreach (var rule in validator.AsEnumerable())
{
    propertyRule = rule as FluentValidation.Internal.PropertyRule;

    if (propertyRule == null)
        continue;

    WebControl control = (WebControl)FindControl("Post" + propertyRule.PropertyName);

    foreach (var x in rule.Validators)
    {
        if (x is FluentValidation.Validators.NotEmptyValidator)
        {
            control.Attributes["required"] = "required";
        }
        else if (x is FluentValidation.Validators.MaximumLengthValidator)
        {
            var a = (FluentValidation.Validators.MaximumLengthValidator)x;

            control.Attributes["size"] = a.Max.ToString();
            control.Attributes["minlength"] = a.Min.ToString();
            control.Attributes["maxlength"] = a.Max.ToString();
        }

        ...
    }
}
function FooWidget(element) {
    this.$element = $(element);
    this.fillOptions = this.fillOptions.bind(this);
    this.$element.on("click", "[data-action=fillOptions]", this.fillOptions);
}

FooWidget.prototype = {
    constructor: FooWidget,

    fillOptions: function(event) {
        // make ajax request:

        var select = this.$element.find("select:first")[0],
            option = null;

        option = document.createElement("option");
        option.value = "...";
        option.text = "...";
        select.appendChild(option);

        ...
    },

    focus: function() {
        this.$element.find(":input:first").focus();
    }
};
namespace Project.Models.Entities
{
    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Body { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime? ModifiedAt { get; set; }
    }
}
namespace Project.UserControls
{
    public class PostControl : System.Web.UI.UserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (Page.IsPostBack)
            {
                PostValidator validator = new PostValidator();
                Post entity = new Post()
                {
                    // Map form fields to entity properties
                    Id = Convert.ToInt32(PostId.Value),
                    Title = PostTitle.Text.Trim(),
                    Body = PostBody.Text.Trim()
                };
                ValidationResult results = validator.Validate(entity);

                if (results.IsValid)
                {
                    // Save to the database and continue to the next page
                }
                else
                {
                    BulletedList summary = (BulletedList)FindControl("ErrorSummary");

                    // Display errors to the user
                    foreach (var failure in results.Errors)
                    {
                        Label errorMessage = FindControl(failure.PropertyName + "Error") as Label;

                        if (errorMessage == null)
                        {
                            summary.Items.Add(new ListItem(failure.ErrorMessage));
                        }
                        else
                        {
                            errorMessage.Text = failure.ErrorMessage;
                        }
                    }
                }
            }
            else
            {
                // Display form
            }
        }

        ...
    }
}
using FluentValidation;
using Project.Models.Entities;

namespace Project.Models.Validators
{

    public class PostValidator : AbstractValidator<Post>
    {
        public PostValidator()
        {
            RuleFor(p => p.Title)
                .NotEmpty()
                .Length(1, 200);

            RuleFor(p => p.Body)
                .NotEmpty();
        }
    }
}
  • Don't Repeat Yourself (which can be more difficult with WebForms)
  • Form validations, which are often extensions of Business Rules that should be managed in back end code
  • Usability enhancements, such as drop down menus, automatically capitalizing text when moving focus away from a text field, etc.
  • User interaction management, which is likely driven by business rules that are not easily done on the back end.

(Note: The code below probably has a few bugs in it, but it should give you the main idea)

Again, the object here is to keep JavaScript and HTML tied together, and not put any JavaScript in C#.

And in your ASP file:

Any validation that requires data from more than one field should not be handled on the client. Do this in C#. Trying to cobble this together in HTML and JavaScript on an ASP page becomes cumbersome and is not enough of a benefit to justify the added overhead and maintenance issues.

I've found client side functionality falls into a couple of categories:

If you can manage this in the ASP file, now you've completely decoupled the front end and back end code!

Now that we have a solid foundation in C#, you can add HTML attributes to each of the form fields and use jQuery Validate to trigger some of the front end validations. You can programatically loop through the FluentValidation rules:

Once you have your basic entities and validators, use them in your code behind:

These JavaScript snippets assist users, and do little to implement business rules. On an application I work on, whenever the user moves focus away from a text box, each word should be capitalized so "foo bar" becomes "Foo Bar". JavaScript and event delegation to the rescue:

This has been the area causing the most pain for me. I'm currently experimenting using FluentValidation with WebForms, and it's actually going pretty well. My best piece of advice regarding validations: Don't use the <asp:Foo /> validators! This is the reason that people complain about WebForms being a copy-and-paste framework. It doesn't have to be that way. Before a quick code example, don't use Data[Set|Table|Row]s either! You get all of the data, but none of the behavior. Use an ORM like Entity Framework or NHibernate, and have all of your ASP pages deal with entity classes, because then you can use something like FluentValidation:

This is the 800 Pound Gorilla of front end development. I like to use a "widget pattern" here, where you write a JavaScript class to encompass the behavior and use HTML attributes and class names as hooks for JavaScript to do its thing.

Note
Rectangle 27 0

c How to avoid writing messy JavaScript in an ASP.NET code behind?


<!-- Use of the DataBind Container/Eval may be useful, but ignoring that.. --!>
<control:BlahBlah Id="ImaControl"
                  OnClientValidate="<%# CreateValidator(ImaControl) %>"/>
function makeValidator(champId, opts) {
    return function (sender, args) {
        // Now this is when it gets harry..
        //
        // Use $get (and $find) inside ASP.NET, especially when
        // dealing with ASP.NET AJAX integration to find a control by ID.
        //
        // HOWEVER, the code uses what appears to be some DevExpress
        // controls and thus must be accessed.. differently, mainly either by
        //   1. `window[clientId]` or
        //   2. `ASPxClientControl.GetControlCollection().GetByName(id);`
        // This is just one of those icky things to deal with; I've shown usage
        // of the former and it may need to be applied to the other controls as well.
        //
        var reasonControl = window[opts.reasonId];        // DX control
        var dynamicControl = $get(opts.dynamicControlId); // normal ASP.NET/DOM
        var errorImage = window[opts.errorImageId];       // DX control
        if(reasonControl.GetText() != '' || dynamicControl.style.display == "none") {
            dynamicControl.className='';
            errorImage.SetClientVisible(false);
            args.IsValid = true;
        }
        // etc.
    }
}
protected string CreateValidator(Control c) {
    var champId = c.ClientID; // example, not necessarily true

    // Then setup other values to supply to the function. While JSON is not
    // *exactly* like a JS object literal it is close enough so we Just Don't Care.
    // I prefer Json.NET from Newtonsoft, but the standard support is just fine.
    // (The champId could also be serialized here, but I chose to show passing
    //  two arguments, one NOT escaped; we assume champId doesn't contain \s or 's.)
    var opts = new JavaScriptSerializer().Serialize(new {
        reasonId = reasonControl.ClientID,
        dynamicControlId = dynamicControl.ClientID,
        errorImageId = Error9.ClientId
    });

    // The use of parenthesis and actual JavaScript returned depends on if the
    // client-side validation property takes JavaScript to execute (common) or if
    // it takes a function to execute later, as found in DevExpress/some libraries.
    // (Remember from above that makeValidator returns a new function.)

    // For DX/DevExpress:
    return string.Format("makeValidator('{0}', {1})", champId, opts);

    // Normal ASP.NET might look like this:
    return string.Format("return makeValidator('{0}', {1}).apply(this, arguments)",
                    champId, opts);
}

@Dave.Lebr1 DataBind needs to be called. See stackoverflow.com/questions/1603398/ , msdn.microsoft.com/en-us/library/ - for controls (using DataBinding syntax) that do not have a declarative DataSourceID set, ensure they have DataBind() called in the PreRender Event.

@brz Because the JavaScript and the Markup/HTML and the C#/code-behind are all separate concerns except for the small area in which values are passed between. In my own applications I use an extension of this approach where the JavaScript lives in it's own file as a nicely packaged module (which is loaded dynamically as needed); this makes it even more isolated from the Markup/HTML. The point is to not use string interpolation, of any sort (except for the very small bridging code) to build the JavaScript - otherwise the result is code that is hard to edit and reason about.

@brz Moving the code to entirely Markup and then data-binding values (e.g. ..; var reasonControl = window['<%# reason.ClientId %>']; ..) is one way to help minimize this coupling, but it still requires a first-step isolation as shown to be a clean and maintainable solution. That is, the data-binding should still only be used to inject values into otherwise valid code and should not be used to generate the code itself. This entire separation is similar to what SQL should use placeholders - it is a separation of values from code that underpins a clean design.

@user2864740 This is a interesting solution. I'm trying to integrate this in my code. I'm trying to add a OnSelectedIndexChanged="<%# CreateValidator(ImaControl) %>" but it doesn't call the codebehind function witch is CreateValidator.

After the first phase we end up with something like (forgive the partial translation, it was hurting my head) the following. Note the use of a closure-builder pattern; in real code I would further have this as a separate module.

And that's the gist of it, bugs included. However, there are number of variations of this approach (including the ASP.NET AJAX ScriptControl magic) and subtle factors to consider; the big point to remember and to strive for is:

It should be clear that the JavaScript code is separate from any string interpolation. It is a normal function, that when called with certain arguments (defined by an API), has a certain behavior. While there are different approaches to "load/inject" this JavaScript (which does matter when UpdatePanels and nested/complex hierarchies come into play), let's pretend that it is currently placed inside a <script> in the markup of the page.

Now, let's wire up the validator to the control - this is entirely fictitious, but it shows the usage of data-binding and actually creating the JavaScript "invocation" in the code-behind, we'll see why in a second. (Using data-binding correctly is actually important as it delays calling the CreateValidator function until the ClientIDs of the controls have been assigned.)

The very first step is to separate out the JavaScript from the code-behind and interpolation of values. Instead of dynamically building JavaScript the approach is then to have a JavaScript function that is given arguments.

Note
Rectangle 27 0

c How to avoid writing messy JavaScript in an ASP.NET code behind?


<dx:ASPxRadioButtonList runat="server" ID="rblistComment">
    <Items>
        <dx:ListEditItem Text="Nouvelle information" Value="0" />
        <dx:ListEditItem Text="Correction de valeurs" Value="1" />
        <dx:ListEditItem Text="Autre" Value="2" />
    </Items>
    <ClientSideEvents SelectedIndexChanged="rblistComment_SelectIndexChanged" />
</dx:ASPxRadioButtonList>
const string csname = "ClientEvents";
const string csurl = "~/js/EtudeCliniqueScript/ClientEvents.js";
Type cstype = this.GetType();

ClientScriptManager cs = Page.ClientScript;

if (!cs.IsClientScriptIncludeRegistered(cstype, csname))
{
    cs.RegisterClientScriptInclude(cstype, csname, ResolveClientUrl(csurl));
}
function rblistComment_SelectIndexChanged(s,e) {

var btnOk = eval($("[id$=btnOK]").attr("id"));
var txtCommentPopup = eval($("[id$=txtCommentPopup]").attr("id"));

btnOk.SetEnabled(s.GetValue() != null);
txtCommentPopup.SetVisible(s.GetValue() == '2');

Client Side Event in the .ascx

Link the javascript file with the user control

After that I add the javascript inside a file called : ClientEvents.js

Finally, in the codebehind I add this code in the Page_Load. So, it register the script and link the user control with the javascript file.

I found a nice solution for the client side events with javascript.

So, basically I add the ClientSideEvent inside the .ascx file. For example, I add the SelectedIndexChanged event. When the index of the radio button change, it call a javascript function that is inside a .js file.

Note