Friday, February 15, 2008

Customizing the ChangePassword control and removing the required CurrentPassword field

It's very rare that what is already provided in asp.net under the Login controls fits my requirements out of the box without some tweaking. Not that any of these controls offer anything specialized, but certainly they are a big time saver if we can re-utilize their functionality.

First some background as to why i personally want to customize the ChangePassword control to suit my needs :

Password recovery is what i was after today, however i have hashed passwords, and recovery is impossible. If the user lost their password, then there is no way for me to know what their password is and send it back in clear text.

The ideal solution is to reset the password, however the autogenerated password is quite ugly and quite hard to remember. What I've decided to do is send the email during password recovery, but as part of the email, instead of telling the user their old password(which i can't).

I'm instead going to ask them to click on a tokenized link that will guarantee to me that they are indeed the ones that requested the password, send them to the page where they can provide a new password, in the background i'd be autogenerating a password first ofcourse, then updating the password with their new password because the MembershipUser.ChangePassword(oldPassword, newPassword) method requires Old password as one of it's two parameters.

This password change step, i'd like to be done using the ChangePassword control, however to my big surprise CurrentPassword Field is a required field that i cannot remove. This is also a field that I do not want asked for during the password change request(since my user has forgotten their password and are now going to provide their new pasword).

There is ofcourse no property or method in this control that removes the CurrentPassword field requirement, below is a screenshot of the ChangePassword control in designview, as you can note, the highlighted field is the CurrentPassword field i do not want.


I've done a quick look on google and in the asp.net/forums and didn't find anybody providing any proper solutions either, mostly vague replies : http://forums.asp.net/p/1189347/2038354.aspx

As you can read from the posts there, the issue seems to be two things which were also my same issues :
1) Remove the current password label/TextBox
2) Pass the new resetpassword to CurrentPassword Property which by the way is a getter only and not settable (SAD SAD)

Both of these things are not supported in this control. So let's quickly fix requirement 1 and there are a couple of ways to fix this :
a) You have to define a custom  <ChangePasswordTemplate>. This can be easily done by taking your ChangePassword control into DesignView in Visual studio, right click on the control and select "Convert to template". You can then switch to HtmlView and set the visibility of CurrentPasswordLabel, CurrentPassword and CurrentPasswordRequired controls.

b) If you prefer to do this in code, then you can find the Label and TextBox for CurrentPassword and set its visiblity to false. Since a is a nobrainer, i'm including a sample code of method (b) :
Label l = (Label)changePassword1.ChangePasswordTemplateContainer.
         FindControl("CurrentPasswordLabel");
if (l != null)
{
    l.Visible = false;
}
TextBox tb = (TextBox)changePassword1.ChangePasswordTemplateContainer.
FindControl("CurrentPassword");
if (tb != null)
{
    tb.Visible = false;
}
RequiredFieldValidator rfv = 
        (RequiredFieldValidator)changePassword1.
ChangePasswordTemplateContainer.FindControl("CurrentPasswordRequired");
if (rfv != null)
{
    rfv.Visible = false;
}

Now that we have the fields we want disabled, let's head onto fix issue 2 :
We can't pass the Autogenerated password to the CurrentPassword Property because its a getter only, however this getter returns the value from our CurrentPassword TextBox, and this job is done immidiately after ChangingPassword event fires. This is good news for us, so we can resolve issue 2 like this :
void changePassword1_ChangingPassword(object sender, 
LoginCancelEventArgs e)
{
    changePassword1.UserName = user.UserName;
    TextBox currentPassword = (TextBox)changePassword1.
    ChangePasswordTemplateContainer.FindControl("CurrentPassword");
    if (currentPassword != null)
    {
        currentPassword.Text = user.ResetPassword();
    }
}

Note that in the above code, user is a reference to a field of type MembershipUser. Ok, that's it. Now we have what were after, look at the screenshot below :

Wednesday, January 30, 2008

Reducing UpdatePanel bloat by utilizing UpdateMode="Conditional" and ChildrenAsTriggers="false"

Just the other day, i was playing around with my DataControls nested inside an updatepanel. While this was working well, since everypostback was being done via an ajax callback, the amount of traffic going back and forth was simply way too bloated. It's easy not to notice at first, because everything is working as expected. however imagine a simple situation as the following pseudo code below. Things could be very complex, depending on how many datacontrols you have and the level of nesting.

<asp:updatepanel id="UpdatePanel1" runat="server">
<ContentTemplate>
    <asp:GridView ID="GridView1" AutoGenerateSelectButton="true" 
runat="server" 
OnSelectedIndexChanged="GridView1_SelectedIndexChanged">
</asp:GridView>

<asp:DetailsView ID="DetailsView1" AutoGenerateEditButton="true"
runat="server" OnModeChanging="DetailsView1_ModeChanging">
</asp:DetailsView>
</ContentTemplate>
</asp:updatepanel>

As you can note from the code, this is a simple GridView, which enables a DetailsView when a row in the GridView is selected. We then have an Edit button on the DetailsView that should send the DetailsView in edit mode when clicked. All nice so far. Now, this is going to work as advertised ofcourse, all postback is done silently in the background.

But if you look closely enough, both the gridview and the DetailsView are contained within a single UpdatePanel, so obviously the postback caused from any child control nested in the updatepanel will cause the entire contents of the updatepanel to refresh and send back the collective rendered content to the client.

Below screenshots is the traffic analysed through firebug (a firefox extention). Hilighted data denotes the extra data we do not need rendered to the client.


As you can note from the screenshots above, my clicking the select button in the gridview, which should be launching the DetailsView in turn, while i'd only need the rendering of the DetailsView send back to me (since the gridview shouldn't need to change), i actually end up with both the rendering of the GridView and the DetailsView. Indeed, there is extra data(the GridView) being rendered which we do not need.


Ok, this is indeed a problem. Were doing things wrongly. So, how do we cause only the DetailsView to render back instead ? One might quickly think, let's put each into their own individual UpdatePanels ? :-)

So, let's try that. Nothing to be ashamed of. It was the first solution that came to my mind too :P
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
     <asp:GridView ID="GridView1" AutoGenerateSelectButton="true" runat="server" 
OnSelectedIndexChanged="GridView1_SelectedIndexChanged">
     </asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>

<asp:UpdatePanel ID="UpdatePanel2" UpdateMode="conditional" runat="server">
  <Triggers>
    <asp:AsyncPostBackTrigger ControlID="GridView1" EventName="RowCommand" />
  </Triggers>
<ContentTemplate>

<asp:DetailsView ID="DetailsView1" AutoGenerateEditButton="true" runat="server" 
OnModeChanging="DetailsView1_ModeChanging">
</asp:DetailsView>
</ContentTemplate>
</asp:UpdatePanel>

And here below is the screnshot of the traffic as seen through firebug. Hilighted data denotes the extra data we do not need rendered to the client.


As you can see, we ended up with the same data as before. Nothing has changed, even though we included them in two separate UpdatePanels. Strange ? Not really. If you think about it, both panels are included in the page, and by default, both panels have UpdateMode="Always" set on them, which causes both to refresh upon an async callback.

azzzz we have a problem indeed. Time to read the documentation :P

The first thing the docs hint about are two things : UpdateMode="Conditional" versus the Default which is "Always" and the second thing is  ChildrenAsTriggers="false" ; both of which are handy.

If my postback was being caused only by children in UpdatePanel2, i couldof just set UpdateMode="Conditional" on UpdatePanel1 and i'd actually achieve what i was after. Only UpdatePanel2's content will be send back to the client. However if you will note in my example above, a control in UpdatePanel1 is the one who is triggering the postback. This satisfies the "Conditional" bit and UpdatePanel1 also renders its contents. Again not what i'm after.

In effect, i've had to set both UpdateMode="Conditional" and also set ChildrenAsTriggers="false" on UpdatePanel1. This stopped the unwanted behaviour. ChildrenAsTriggers property has a proper explaination in the documentation, you can look it up. In short, it simply stops any direct children from making it refresh. This is good for us and what we are after.

That's just perfect. Using this combo, i can keep the panel i do not want updated, while letting the panel with the update trigger refresh at will. This also allows me to control who gets updated by calling the update method manually on the panel that interests me. For example if i edit a record in the detailsview and want to show the change in the gridview, i'd run the update operation and then right after that, call update on the panel that contains my gridview manually.

<asp:UpdatePanel ID="UpdatePanel1" UpdateMode="conditional" 
ChildrenAsTriggers="false" runat="server">
<ContentTemplate>
   <asp:GridView ID="GridView1" AutoGenerateSelectButton="true" runat="server" 
OnSelectedIndexChanged="GridView1_SelectedIndexChanged">
   </asp:GridView>
</ContentTemplate>

</asp:UpdatePanel>
<asp:UpdatePanel ID="UpdatePanel2" runat="server">
   <Triggers>
      <asp:AsyncPostBackTrigger ControlID="GridView1" EventName="RowCommand" />
   </Triggers>
<ContentTemplate>

<asp:DetailsView ID="DetailsView1" AutoGenerateEditButton="true" runat="server" 
OnModeChanging="DetailsView1_ModeChanging">
</asp:DetailsView>
</ContentTemplate>
</asp:UpdatePanel>

here are the screenies, you can see, only the detailsview is now present in our callback. Just perfect.


While this is a simplistic example, had you many deeply nested updatepanels you can easily workout who gets updated and whose data gets rendered reducing bloat, using the same method i've mentioned above. Don't simply include EVERYTHING in one updatepanel or multiple and depend on the default, posting back un-necessary bloat on each callback. Firebug for Firefox and Fiddler for IE are both great tools for inspecting and analysing your callback traffic. Use either. I prefer firebug :p

Ok, so that was easy(just set UpdateMode="conditional" ChildrenAsTriggers="false"), nonetheless i ended up with quite a lengthy post :x

Friday, January 4, 2008

Failed to load viewstate ? Typical problem, with an obvious solution.

  Understanding viewstate is fundamental in asp.net, especially if you had run into :

Failed to load viewstate. The control tree into which viewstate is being loaded must match the control tree that was used to save viewstate during the previous request. For example, when adding controls dynamically, the controls added during a post-back must match the type and position of the controls added during the initial request.

The only way to resolve is a proper understanding of viewstate.

http://geekswithblogs.net/FrostRed/archive/2007/02/17/106547.aspx is a interesting post on viewstate that i happen to read today, pointed out to me by someone who ran into a viewstate problem about the control tree not matching and was clearly afraid of adding controls dynamically after reading some facts presented in that article. Who wouldn't ?

While the post gives us a very good understanding of viewstate and how it can fail, so i encourage you to read it first, might seem lengthy but I assure  you, it's quite interesting. However, when you're done, follow my rant here, since I feel it's important to know, that, the failure can only happen when either done deliberately as per the sample code in the post i linked to above or to *not* understanding viewstate and how it works.

So how can we easily avoid these failures ? Let's look at his first code example, and build onto that :
protected void Page_Init(object sender, EventArgs e)
{
if (!IsPostBack)
{
Button btnClickMe
= new Button();
form1.Controls.Add(btnClickMe);
btnClickMe.Text
= "Click me";
}
else
{
Label label
= new Label();
form1.Controls.Add(label);
}
}

As you can note above, this is problematic, since the control into which viewstate is restored is matched by control index, so when the index changes, as is clear in the above code, because if btnClickMe was loaded in for example index [0], now upon postback, after the page has been recreated and rebuilt, the Label "label" is loaded in index [0] instead and takes the place of the button. So this means viewstate that was meant for the button is loaded into the label instead, and the output in the screen after clicking the button is "click me" which was clearly not provided to the label's text property.

Now that we understand the problem, how can this sample apply in real world or why would anybody want to do something like this ? Basically in short, why is viewstate being utilized, if it's not needed after postback ? Button btnClickMe is not reloaded after postback, so it's safe to turn off viewstate on this control, and problem is solved.

This is a typical situation where you deliberately want viewstate to fail, apart from that i see no real use to want to maintain viewstate, which is also bloat on a control that clearly is not utilizing it.

so a rewrite ? here :
protected void Page_Init(object sender, EventArgs e)
{
if (!IsPostBack)
{
Button btnClickMe
= new Button();
// note the addition of the following line
btnClickMe.EnableViewState = false;
form1.Controls.Add(btnClickMe);
btnClickMe.Text
= "Click me";
}
else
{
Label label
= new Label();
form1.Controls.Add(label);
}
}

Otherwise, again as per the sample code above, had we been using viewstate, then the problem would resolve itself, if we recreated the control also after postback, which is one of the basic rules of dynamic controls creation. I say rules but really it's the logical thing to do since the page is destroyed after postback and asp.net will have no recollection of controls you added dynamically since memory is cleared, so it's upto you to build it up again manually.


protected void Page_Init(object sender, EventArgs e)
{
// button will be created even after postback
Button btnClickMe = new Button();
btnClickMe.EnableViewState
= false;
form1.Controls.Add(btnClickMe);
btnClickMe.Text
= "Click me";
if (IsPostBack)
{
Label label
= new Label();
form1.Controls.Add(label);
}
}

So, bottom line, a proper understanding of viewstate, knowledge of the page life cycle, so you know in what phase it's safe to build your control, which will guarantee that viewstate is reloaded into the control(so you load it prior to page_load), and you got it right. For a proper understanding of the page life cycle, you can read the following document on msdn : http://msdn2.microsoft.com/en-us/library/ms178472.aspx?wt.slv=ColumnA

Update Jan/04/2008: I forgot to mention a gotcha, so here it is :  

Another gotcha you want to avoid is also the order of controls, that is, when you're loading a dynamic control, make sure the order in which you create it, has the same order when you recreate it. Confused, here let me explain better :
protected void Page_Init(object sender, EventArgs e)
{
if (IsPostBack)
{
Label label
= new Label();
label.ID
= "label1";
form1.Controls.Add(label);
label.Text
= "label";

Button btnClickMe
= new Button();
btnClickMe.ID
= "button1";
form1.Controls.Add(btnClickMe);
btnClickMe.Text
= "Click me";
}
else if (!IsPostBack)
{
//Now lets change the order
//during postback and we are
//recreating the controls
Button btnClickMe = new Button();
btnClickMe.ID
= "button1";
form1.Controls.Add(btnClickMe);
btnClickMe.Text
= "Click me";

Label label
= new Label();
label.ID
= "label1";
form1.Controls.Add(label);
label.Text
= "label";
}
}

As you can note above, the order in which controls are added changes after postback. In this scenario what really happens ? The viewstate meant for the button is loaded into the label and the viewstate meant for the label is loaded into the button. So, you really want to be careful with the order in which you recreate your controls.

Tuesday, January 1, 2008

UpdatePanel Css StyleSheet upon partial-refresh bug in IE

The update panel seems to have a bug when registering an external stylesheet or including css styles from within the contents that will be getting partially rendered. The bug only seems to occur in IE, works nicely in firefox. Impressive indeed. My problems started when i had a control that needed to render a link to an external stylesheet, which was quite mm easy and normal.

I mean i've been there and done that plenty of times, however this time there were situations in which the stylesheet needed to be registered if my control was included in an updatepanel and kept invisible during inital load, while enabling it only upon a partial postback. TRICKY TRICKY TRICKY!

More over, there is an old bug opened and closed with a reason "this is by design". Seems awkward to me that this is by design and only effects IE :-(
The url to the bug report is here :


I resolved by registering the css in the OnInit phase of my custom control. Since this would run and register the css even if the control was disabled or invisible, which is what i was after, since it registered the control with the page on first load instead of trying to rendering the style link as part of my rendering for the control(which obviously didn't work in IE). A simplied piece of my code of how i have worked around this problem is as follows :
protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
ScriptManager sm = ScriptManager.GetCurrent(Page);
    if (!sm.IsInAsyncPostBack)
{
       string css = string.Format("<link rel=\"stylesheet\" 
        href=\"{0}\" type=\"text/css\" />", 
ResolveUrl(CssClassFile));

ScriptManager.RegisterClientScriptBlock(this, 
      typeof(MyBlahControl), "MyBlahId", css, false);
}
}

Update 01/01/2008 : Please read the first two comments below. CSS contianment from within the <body element violates xhtml specs and as such here is an update that includes the css in the <head section. Thanks to Ram Krisna for pointing out/commenting this.
protected override void OnInit(EventArgs e)
{
  base.OnInit(e);
ScriptManager sm = ScriptManager.GetCurrent(Page);
  if (!sm.IsInAsyncPostBack)
{
HtmlLink l = new HtmlLink();
l.Href = ResolveUrl(CssClassFile);
l.Attributes.Add("rel", "stylesheet");
l.Attributes.Add("type", "text/css");
Page.Header.Controls.Add(l);
}
}

A simplified test of what I feel is an open bug and should be fixed can be seen below. After clicking the button, the style applied to the label is lost and happens only in IE7, donno about previous versions since i have not tested :
<%@ Page Language="C#" %>

<script runat="server">

protected void Button1_Click(object sender, EventArgs e)
{
// do something
}

</script>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 
Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<!-- 
Note below that for simplicity i am not
using an external stylesheet. Anyway, even with an external style
sheet the result is the same. The style is 
not applied after partial postback 
-->
<style type="text/css">
.MakeGreen{background-color:green;}
</style>
<asp:Label ID="Label1" CssClass="MakeGreen" 
runat="server" Text="Label"></asp:Label>

<asp:Button ID="Button1" runat="server" Text="Partial refresh"
OnClick="Button1_Click" />

</ContentTemplate>
</asp:UpdatePanel>
</div>
</form>
</body>
</html>

Tuesday, October 9, 2007

Custom Serverside paging in GridView Vs DataGrid

 When doing serverside paging(that is paging at the database layer by returning only the paged result), one of the things I miss in the GridView control is the VirtualItemCount, which is supposedly only supported in the older control's like the DataGrid.

This property was quite useful because while being able to supply to the DataGrid a variable number of paged result sets, i was also able to tell the DataGrid, the total number of records, that way it knew how many pager buttons to display.

Eg. If we had, say a total of a 100 records and had the pageSize set to 7, so only 7 records are shown at a time, then how does the grid know how many numbered pager buttons to display allowing us to navigate from one page to another ? That's where the VirtualItemCount came into play and saved the day. To this property we'd pass a total records count and that was it. In the GridView today ? There is no VirtualItemCount present. The way it were planned it seems is to use the ObjectDataSource, which in my honest opinion is simply extra work, however it does abstract much of this code nicely and put it where it should be, in the data tier.

In all the example code in this post, I shall be using the MemberShip.GetAllUsers method. This method is overloaded and can retrieve a paged result of users Versus returning all the user's in the database, which is a quite handy overload and works out nicely for the code i want to use in this post.

Let's look at a simple example of how we couldof performed custom paging on the DataGrid control back in the old days :


int virtualItemCount = 0;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
DataGridMembers.DataSource
= Membership.GetAllUsers(0,
DataGridMembers.PageSize,
out virtualItemCount);
DataGridMembers.VirtualItemCount
= virtualItemCount;
DataGridMembers.DataBind();
}
}

Note the VirtualItemCount ? Then on the PageIndexChanged event of the DataGrid we did :

protected void DataGridMembers_PageIndexChanged(object source, 
DataGridPageChangedEventArgs e)
{
DataGridMembers.CurrentPageIndex
= e.NewPageIndex;
DataGridMembers.DataSource
= Membership.GetAllUsers(e.NewPageIndex,
DataGridMembers.PageSize,
out virtualItemCount);
DataGridMembers.DataBind();
}

And that was it, it was as simple as that.

Now try to do that on the GridView ? Can't be done and this is a control that is replacing the old DataGrid control. To make things worse, the DataGrid is not a supported control anymore in 2.0 ; It's been obsoleted and by default you wont even find this control in your toolbox. You can still use it however by manually adding it to your toolbox. Unfortunate, because there are moments like this custom paging situation and i'm getting nostalgic already.

So, how to achieve the same thing in the GridView control which happens to replace the DataGrid ? Well, it's a long shot. Since we cannot achive this directly on the GridView, we are going to have to do it via the DataSource control, which is actually the control that is populating the data for the gridview and also the control that handles paging and sorting amoung other things. While i like this kind of data abstraction, i'm actually doing more work and making the extra effort,but this is how you would implement custom paging on  your GridView control.
The GridView alone is lacking a VirtualItemCount property, which i believe shouldn't have been so hard to implement. To compensate for this lacking, you perform custom serverside paging by using an ObjectDataSource control, defining a SelectMethod and a SelectCountMethod method. The SelectCountMethod is your custom method that returns the Total records count.

So let's look at some code, and there are few gotcha's that weren't exactly obvious to me in the begining :
First our custom SelectMethod :

public MembershipUserCollection GetAllUsers(int startRowIndex, 
int maximumRows)
{
if (startRowIndex > 0)
startRowIndex
= startRowIndex / maximumRows;
return Membership.GetAllUsers(startRowIndex,
maximumRows,
out selectCountValue);
}

One gotcha you want to make note of is how i have some extra code to divide startRowIndex by maximumRows ; This is because startRowIndex is actually the first row in the resultset as the variable name indicates, however what i really need is the current page index, because that is what our stored procedure is expecting, in this case that is what the internal MemberShip.GetAllUsers method is expecting.

Next we need to add a SelectCountMethod :

int selectCountValue = 0;
public int SelectVirtualCount()
{
return selectCountValue;
}

The code is minimum as you can note, but ofcourse, there is some extra effort to making the abstraction. The code above goes into the data layer.

And lastly, we need to subscribe to PageIndexChanging event of our GridView and pass the selected page index :

protected void GridViewMembers_PageIndexChanging(object sender, 
GridViewPageEventArgs e)
{
GridViewMembers.PageIndex
= e.NewPageIndex;
}

A peculiar behaviour you will notice is that the SelectCountMethod and the SelectMethod both share the same SelectParameters if SelectParameters are defined. Peculiar because i was not really expecting it, however I have no issues with it, For example if we had to rewrite our previous example to include also a search by userName, then our ObjectDataSource would be expecting some SelectParameters like this :

<asp:ObjectDataSource ID="ObjectDataSourceMembers"
EnablePaging
="True"
SelectCountMethod
="SelectVirtualCount"
SelectMethod
="GetAllUsers"
TypeName
="MembersData"
runat
="server">
<SelectParameters>
<asp:ControlParameter ControlID="TextBoxUserName" Name="userName"
PropertyName
="Text" DefaultValue="All" />
</SelectParameters>
</asp:ObjectDataSource>

And then modified our SelectMethod as such :

public MembershipUserCollection GetAllUsers(int startRowIndex, 
int maximumRows, string userName)
{
if (startRowIndex > 0)
startRowIndex
= startRowIndex / maximumRows;
if (userName == "all")
{
return Membership.GetAllUsers(startRowIndex,
maximumRows,
out selectCountValue);
}
else
{
return (MembershipUserCollection)
Membership.FindUsersByName(userName
+ "%",
startRowIndex, maximumRows,
out selectCountValue);
}
}

As you can see while our GetAllUsers(SelectMethod) has the needed userName parameter, our SelectVirtualCount method defined above does not have any parameters defined on it, since we don't need to pass it anything. However, the ObjectDataSource is going to complain with :

ObjectDataSource 'ObjectDataSourceMembers' could not find a non-generic method 'SelectVirtualCount' that has parameters: userName

So it means both the SelectMethod and the SelectCountMethod share the same Select parameters. I resolved by adding the extra userName parameter in the SelectCountMethod as well, while i did not clearly need it, but no big deal.
Here is what the modified SelectCountMethod wouldof looked like :

int selectCountValue = 0;
public int SelectVirtualCount(string userName)
{
return selectCountValue;
}

The SelectCountMethod is treated exactly in the same way the SelectMethod is treated, so the ObjectDataSource's Selected Event is going to fire twice for example, once when the SelectMethod is called and once when the SelectCountMethod is called. These are all gotchas i was not really prepared for.

Full code for custom serverside paging in DataGrid :

<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<script runat="server">
int virtualItemCount = 0;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
DataGridMembers.DataSource
= Membership.GetAllUsers(0,
DataGridMembers.PageSize,
out virtualItemCount);
DataGridMembers.VirtualItemCount
= virtualItemCount;
DataGridMembers.DataBind();
}
}

protected void DataGridMembers_PageIndexChanged(object source,
DataGridPageChangedEventArgs e)
{
DataGridMembers.CurrentPageIndex
= e.NewPageIndex;
DataGridMembers.DataSource
= Membership.GetAllUsers(e.NewPageIndex,
DataGridMembers.PageSize,
out virtualItemCount);
DataGridMembers.DataBind();
}
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:DataGrid ID="DataGridMembers" AllowPaging="True"
AllowCustomPaging
="true" PageSize="2" runat="server"
OnPageIndexChanged
="DataGridMembers_PageIndexChanged">
<PagerStyle Mode="NumericPages"
HorizontalAlign
="Right" />
</asp:DataGrid>
</div>
</form>
</body>
</html>

And the full code for custom paging in GridView :

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>

<script runat="server">
protected void GridViewMembers_PageIndexChanging(object sender, 
GridViewPageEventArgs e)
{
GridViewMembers.PageIndex
= e.NewPageIndex;
}
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>

<asp:GridView ID="GridViewMembers" DataSourceID="ObjectDataSourceMembers"
runat
="server" AllowPaging="True" PageSize="2"
OnPageIndexChanging
="GridViewMembers_PageIndexChanging">
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSourceMembers"
EnablePaging
="True"
SelectCountMethod
="SelectVirtualCount"
SelectMethod
="GetAllUsers"
TypeName
="MembersData"
runat
="server"></asp:ObjectDataSource>
</div>
</form>
</body>
</html>
public class MembersData
{
public MembersData()
{
//
// TODO: Add constructor logic here
//
}
int selectCountValue = 0;
public int SelectVirtualCount()
{
return selectCountValue;
}
public MembershipUserCollection GetAllUsers(int startRowIndex,
int maximumRows)
{
if (startRowIndex > 0)
startRowIndex
= startRowIndex / maximumRows;
return Membership.GetAllUsers(startRowIndex,
maximumRows,
out selectCountValue);
}
}

Thursday, October 4, 2007

Rich ajax applications that do not break if javascript is disabled.

Always wanted to test if the client's browser has javascript disabled in their browser ? Sounding impossible ? Well, not anymore!

Ok, so as Web2.0 applications are increasing in popularity and we are all starting to enable a lot of clientside javascript in our web applications, especially the use of ajax extentions and the updatepanel, we also know how this is all going to break for users who have javascript turned off or whose browsers do not support javascript.

Now, lets be real, it's not very common nowadays for a useragent to not support javascript, all the big players support it well(IE, Firefox, Opera, Safari).

However what about users that have javascript turned off ? Personally, I don't want my applications breaking on potential clients who fall in this category and realistically, today, there is no way to test for sure if the client turned off javascript. Sure, I am aware of the HttpBrowserCapabilities class, specifically the EcmaScriptVersion property. However this is a pretty useless property to me in this usage scenario since it only tells me what version of javascript the users client browser supports. This is simple static information passed on by the clientbrowser when making the request.

What we need is to know "Is javascript turned off ?" Knowing this is important in today's applications because it allows us to gracefully exit, or provide a serverside alternative, a non script version of our page being requested.

As of this writing I have not found any clear answer anywhere, so i have invested a couple of minutes to come up with the following solution. It's quite simple and should be working 100% without breaking, telling us exactly what we are seeking. -->> Hey, you there making the request, do you have javascript enabled/disabled ?

The idea is to introduce a piece of js code when the first request is being made by the client. Particularly a piece of code that can tell us serverside, "yes, javascript is enabled", without any delay or rendering much content to the client.

<script type="text/javascript">
window.location.href
='http://weblogs.asp.net/Default.aspx?supportsjs=true';
</script>

if javascript is enabled, the clients page will redirect, but what if js is disabled ? this script wont do anything, so to address this, we will introduce a second piece of code as well :

<meta http-equiv="refresh" 
content
="0;url=http://weblogs.asp.net/Default.aspx?supportsjs=false" />

That's it. If js is disabled, this second line will kick off and postback. Perfect.

Still, we don't want to do this everytime. We want to do this for the first request only, so what we can do is set a serverside flag in session state. I don't really recommend using session state because I find sessions to be unreliable, but that's just me. You basically have no control on when the session recycles. My preference lies in the profile provider(which is going to persist the value in the database and in style), however for simplicity, i'm setting a flag in session state.


Update october 4th,2007 : Initially, as you have read above in this post, i was planning on using the meta refresh tag if js was indeed disabled on the client. It was also a useless effort, when I couldof just assumed js was disabled from the start and only enabled if the js script fired the postback. As you will note from Richards comment below, the meta refresh tag only brings other problems to the table as it can in turn be disabled invidividually by the browser and not only in IE or Firefox but opera has support for this too. Here is the updated code that checks for javascript without meta refresh tag getting in the way.

Following is the code :

<%@ Page Language="C#" %>

<script runat="server">

/*
do it in init, otherwise you cannot
set ScriptManager.EnablePartialRendering property
*/
protected void Page_Init(object sender, EventArgs e)
{
bool notSet = string.IsNullOrEmpty(this.Request.QueryString["supportsjs"]);
string url = this.Request.Url.OriginalString;
if (notSet && Session["supportsjs"] == null)
{
string queryStringKey = (this.Request.QueryString.Count > 0) ?
"&amp;supportsjs=" : "?supportsjs=";

string jsTesterScript = string.Format(
"<{0} type=\"text/javascript\">window.location.href='{1}{2}true'</{0}>",
"script", url, queryStringKey);

Response.Write(jsTesterScript);
Response.Flush();
}

if (Session["supportsjs"] == null)
Session[
"supportsjs"] = false;// default, assume false

// if our js code posted back,
// and we still hold default assumption,
// then client does indeed support js
// so update the session

if (!notSet && !(bool)Session["supportsjs"])
Session[
"supportsjs"] = true;
ScriptManager1.EnablePartialRendering
= (bool)Session["supportsjs"];
}

</script>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" 
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"
>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<h1>outside updatepanel : <%= DateTime.Now %></h1>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<h1>Inside updatepanel : <%= DateTime.Now %></h1>
<asp:Button ID="Button1" runat="server" Text="Button" />
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>

I have tested the above code to work flawlessly in IE, Firefox, Opera and Safari. I've enabled and disabled javascript in the browsers during testing and as simple as the code may seem, it works. This is indeed great because now your rich ajax web applications can cater nicely to all targets and not break in the face of who has javascript disabled.