Friday, July 18, 2008

Use JavaScript on Controls in a GridView, DetailsView or FormView (even in a MasterPage with an UpdatePanel)

Ever want to use JavaScript on a Textbox inside a DetailsView? It can be done, you just need to overcome two issues. The first is that .Net will change the ID from "MyTextBox" to something like "ct100_ContentPlaceholder1_DetailsView1_MyTextBox". The second is that these controls may exist only in Edit or Insert mode.

The first thing we want to do is get the ID of MyTextbox that gets sent to the browser. Since MyTextbox only exists when we're editing or inserting, we need to use the PreRender event. We check to make sure we are in Edit/Insert mode and then use FindControl to get a handle on the textbox. Next we use the ScriptManager RegisterHiddenField method to register a hidden control that has the ClientID as its value.

Protected Sub DetailsView1_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles DetailsView1.PreRender
    If DetailsView1.CurrentMode = DetailsViewMode.Edit Or DetailsView1.CurrentMode = DetailsViewMode.Insert Then
        Dim txt As TextBox = DetailsView1.FindControl("MyTextbox")
        ScriptManager.RegisterHiddenField(DetailsUpdatePanel, "MyTextboxClientID", txt.ClientID)
    End If
End Sub

There's several important things to notice here. First, ScriptManager is used to register the hidden field since I'm assuming you are in an UpdatePanel. Look at ClientScript to add the hidden field if you're not using an UpdatePanel. Also, notice that I pass the UpdatePanel as the first parameter instead of the page.

Second, we dynamically register the hidden field. We don't use a HiddenField control because it will suffer from the same naming issue in a MasterPage.

Third, the hidden field is named MyTextboxClientID. We'll need that name in the JS to get our control. The following function takes the name and returns the control. If the hidden field or the control aren't present, it returns null. This keeps an error from getting thrown when we're not in Edit/Insert mode.

// Gets a contained control from a
// hidden field.
// name - The name of the hidden field
//        that has the ClientID name.
function GetContainedControl(name)
{
    var hdn = document.getElementById(name);
    if (hdn!=null)
        {return document.getElementById(hdn.value);}
    return null;
}

Here's how to call GetContainedControl. Notice we pass the name of the hidden field and not the name of the control we want to find. We check to see if it's null and we're ready to go.

var txt = GetContainedControl("MyTextboxClientID")
if (txt != null)
{
    // Do stuff here
}

Lastly, I'd recommend using constants for these names so that you don't get a name mismatch between the code behind and the JS. Here's the revised lines to use a constant.

Protected Const MyTextboxClientID As String = "MyTextboxClientID"

Protected Sub DetailsView1_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles DetailsView1.PreRender
    If DetailsView1.CurrentMode = DetailsViewMode.Edit Or DetailsView1.CurrentMode = DetailsViewMode.Insert Then
        Dim txt As TextBox = DetailsView1.FindControl("MyTextbox")
        ScriptManager.RegisterHiddenField(DetailsUpdatePanel, MyTextboxClientID, txt.ClientID)
    End If
End Sub

And the JavaScript.

var txt = GetContainedControl('<% = MyTextboxClientID %>')
if (txt != null)
{
    // Do stuff here
}

Hope that helps!!!!

Thursday, July 10, 2008

LINQ To SQL Row not found or changed

LINQ To SQL likes to use TimeStamp columns for version tracking and makes it really easy to do so. However, in a lot of the apps I work on, we need to keep track of when the last change was made so we have a DateTime column titled LastUpdated. Using Steve Michelotti's post, I was able to use this column for row version control.

Everything was fine until I was using a DetailsView and got the message "Row not found or changed". This message is LINQ's way of telling you that the row has been changed and you now have a concurrence conflict. Since I was working on my local box, I knew this wasn't the case. Pulling up SQL Profiler, I was able to see that LINQ was sending the LastUpdated date back without milliseconds. Since this isn't the same as the value on the row, it thinks a change has occurred.

I was displaying the LastUpdated field in a readonly column so it shouldn't have been getting changed. I also had it as DataKey and could see that it was getting passed back. After farting around for a while, I finally figured out that it was the Label control in the EditItemTemplate that was causing the problem.

<asp:Label ID="Label3" runat="server" Text='<%# Bind("LastUpdated") %>'></asp:Label>

The Bind in the above causes the DetailsView to use the value displayed in the label as the value for the update. Bind is two way even though the column is marked ReadOnly and ReadOnly really means "Don't let the user change anything." It doesn't mean "Don't let the value change."

Simply changing the Bind to Eval solves the problem as below.

<asp:Label ID="Label1" runat="server" Text='<%# Eval("LastUpdated") %>'></asp:Label>

I haven't tested but this should affect all of the other controls such as GridView and FormView.

Cheers

Wednesday, July 2, 2008

JavaScript DateDiff Functions

Here's some JavaScript functions that I use like VB's DateDiff function. I've seen some other functions but most take the number of days and end up close but not quite. They also don't correctly handle leap years.

DateDiffYears will return the number of years between two dates while DateDiffMonths will return the number of months. Couple of things to remember. DateDiffYears is often used to determine age so it won't go negative but will return zero. Thus, if the end date precedes the start date you'll get zero as the result. Also, pass start and end as date objects rather than strings. LAstly, you need DateDiffYears to use DateDiffMonths.

// Calcuates the number of years between two dates.
// start - a date object that defines the beginning of the range
// end - a date object that defines the end of the range
function DateDiffYears(start, end)
{
    var years = end.getFullYear() - start.getFullYear();
    var month1 = start.getMonth()
    var month2 = end.getMonth()
    var day1 = start.getDate()
    var day2 = end.getDate()
    if (month2 < month1)
        {
            years -= 1;
        }
    else
        {
            if (month2 = month1)
                {
                    if (day2 < day1)
                        {years -= 1;}
                }
        };
    
    if (years < 0)
        {years=0;}
        
    return years;
}

// Calcuates the number of months between two dates.
// start - a date object that defines the beginning of the range
// end - a date object that defines the end of the range
function DateDiffMonths(start, end)
{
    var years1 = DateDiffYears(start, end);
    var years2 = end.getFullYear() - start.getFullYear();
    var months = years1 * 12
    var month1 = start.getMonth()
    var month2 = end.getMonth()
    var day1 = start.getDate()
    var day2 = end.getDate()
    if (years1 != years2)
        {
            months = months + 11 - (month1 - month2);
        }
    else
        {
            months = months + (month2 - month1);
            if (day2 < day1)
                        {months -= 1;}
        };
        
    return months;
}

Someday I will combine them into one DateDiff function but that's a later entry.

Technorati Tags: ,,