I'm questionning about what is the best practice to use Javascript with ASP.NET.
I don't know if it's the best practice but I add the javascript client side event inside the codebehind. It is working correctly but is this the best practice?
For example, I got a radio button control and I add the Javascript client side event in the Page_Init. The page init can be recalled multiple time so the Javascript will be rendered each time that the Page_It is called.
Also, it is hard to debug a long Javascript string. How it can be more clean ... is there a way?
Let see an example of a variable that contains Javascript :
scripts.Text += "<script type='text/javascript'>function ValidateDdl" + metachamp.ID +
"(sender, args) { if(" + txtReason.ClientID + ".GetText() != '' ||" +
dynamicControl.ClientID +
".style.display == 'none' || HiddenFieldSaveError.Contains('" + metachamp.ID +
"') ){" + dynamicControl.ClientID + ".className='';HiddenFieldError.Remove(" +
metachamp.ID + ");" + errorImage.ClientID +
".SetClientVisible(false);args.IsValid = true;}else{var comboVal = document.getElementById('" +
Image9.ClientID + "'.substring(0,'" + Image9.ClientID +
"'.length - 6) + 'ddl').value ;if (comboVal != '0' ) {args.IsValid = true;HiddenFieldError.Remove(" +
metachamp.ID + ");" + validImage.ClientID +
".SetClientVisible(false);HiddenField.Remove('Bypass-' + '" +
metachamp.ID.ToString() + "');HiddenFieldSaveError.Remove(" + metachamp.ID +
");" + dynamicControl.ClientID + ".className='';" + errorImage.ClientID +
".SetClientVisible(false);}";
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.
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.
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.
}
}
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.)
<!-- Use of the DataBind Container/Eval may be useful, but ignoring that.. --!>
<control:BlahBlah Id="ImaControl"
OnClientValidate="<%# CreateValidator(ImaControl) %>"/>
And then back to the code-behind:
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);
}
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:
Separate the JavaScript code and use an API to communicate values.
This is a classic question for any technology stack. To answer this question, I keep a couple things in mind:
- Don't Repeat Yourself (which can be more difficult with WebForms)
- Do one thing, and do it well
I've found client side functionality falls into a couple of categories:
- 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)
Form Validations With ASP.NET WebForms
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:
App_Code/Models/Entities/Post.cs
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; }
}
}
App_Code/Models/Validators/PostValidator.cs
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();
}
}
}
Once you have your basic entities and validators, use them in your code behind:
UserControls/PostControl.ascx.cs
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
}
}
...
}
}
UserControls/PostControl.ascx
<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" />
Programmatically Adding Client Side Validations
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:
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();
}
...
}
}
Complex, Multi Field Validations
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.
Usability Enhancements
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:
Scripts/foo.js (imported on each page)
$(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();
});
});
To disable this behavior:
Code Behind:
PostTitle.Attributes["data-capitalize-disabled"] = "true";
ASP:
<asp:TextBox ... data-capitalize-disabled="true" />
If you can manage this in the ASP file, now you've completely decoupled the front end and back end code!
User Interaction Management
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.
Scripts/FooWidget.js
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();
}
};
And in your ASP file:
<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>
Again, the object here is to keep JavaScript and HTML tied together, and not put any JavaScript in C#.
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.
Let see:
Client Side Event in the .ascx
<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>
After that I add the javascript inside a file called : ClientEvents.js
Add javascript code
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');
}
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.
Link the javascript file with the user control
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));
}