Wednesday, 29 June 2011

Combining resources

It is recommended that you download all your scripts/styles in a single request to the server, instead of getting files one by one, thus blocking the browser.

So, instead of:


   <script type="text/javascript" src="../Scripts/romama.js"></script>
   <script type="text/javascript" src="../Scripts/notes.js"></script>

I really want to have something like this:

   <script type="text/javascript" src="../Scripts/combined.js"></script>


Even if there are ready-to-use solution available, like Yahoo! combo handler, this never stopped me before from implementing my own custom solution.

What I want for myself is to be able to put one single script tag in a master page and have all my scripts downloaded as a single file.

    <script type="text/javascript" src="CombineResources.axd?type=script"></script>

I am going to achieve this with the custom HttpHandler, which will know which resources are needed for any particular page. Here is implementation:

public class CombineResources : IHttpHandler
{
       #region Static Members

       private static Dictionary<string, List<string>> ScriptMap = new Dictionary<string, List<string>>()
       {
              {"/Notes/CategoryEditor.aspx", new List<string>() {"/Scripts/romama.js", "/Scripts/categoryEditor.js"}}
       };

       #endregion

       #region IHttpHandler Members

       public bool IsReusable
       {
              get { return true; }
       }

       public void ProcessRequest(HttpContext context)
       {
              string resourceType = context.Request.QueryString["type"] as String;
              if (String.IsNullOrWhiteSpace(resourceType))
              {
                     return;
              }

              string localPath = context.Request.UrlReferrer.LocalPath;
              string combined = context.Cache[localPath + ":" + resourceType] as String;
              if (combined != null)
              {
                     context.Response.Write(combined);
                     return;
              }

              List<string> resources = new List<string>();
              if (resourceType == "script")
              {
                     if (ScriptMap.ContainsKey(localPath))
                     {
                           resources = ScriptMap[localPath];
                     }
              }
              else if (resourceType == "style")
              {
                     // TODO:
              }
              else
              {
                     throw new InvalidOperationException(String.Format("unrecognized resource type: {0}", resourceType));
              }

              StringBuilder sb = new StringBuilder();
              foreach(var resource in resources)
              {
                     sb.AppendLine(File.ReadAllText(context.Server.MapPath("\\") + resource));
              }
              sb.AppendLine(@"// Generated: " + DateTime.Now.ToLocalTime());
              context.Response.Write(sb.ToString());
              context.Cache.Insert(localPath + ":" + resourceType, sb.ToString(), null, DateTime.UtcNow.AddMinutes(1), Cache.NoSlidingExpiration);
       }

       #endregion
}

The thing I need to look into is whether the scripts going to be cached in a browser.

Touch me

Romama on iPad

Thursday, 16 June 2011

State, still

How we want to see it working:

a. User requests page
b. Page is sent to user
c. User clicks buttons, page handles events


How it is working in reality:

a. User requests page
b. Page is sent to user and discarded
c. User clicks buttons, new page handles events




How to make the reality look like a dream:

1. Get the data from the database and render dynamic controls in Page_PreRender 
2. Save the data which is used to render controls between requests in a ViewState
3. When user submits the page back to server (Page.IsPostback == true), recreate controls as they were, from the ViewState
4. Happily handle all the events
5. Discard all restored controls in Page_PreRender and start all over again.



As I understand, this is how (more or less) databound server controls are working.

Wednesday, 15 June 2011

Stateless means stateless

Or how to screw up everything without true understanding of the technology.

Let's say we want show some dynamic data on the web page, like notes. Notes will be represented by the NoteControl which will be a bunch of textboxes, labels and buttons:


<asp:TextBox ID="TitleTextBox" runat="server"></asp:TextBox>
<asp:TextBox ID="NoteTextTextBox" runat="server" TextMode="MultiLine"></asp:TextBox>
<asp:ImageButton
ID="AcceptNoteButton"
runat="server"
ImageUrl="~/Resources/Icons/64x64/circle-check.png"
AlternateText="Save"
CausesValidation="true" />



 and will be rendered to something like that:


<input name="ctl00$MainContent$NotesView$ctl03$TitleTextBox" type="text" id="MainContent_NotesView_ctl03_TitleTextBox" />
<textarea name="ctl00$MainContent$NotesView$ctl03$NoteTextTextBox" id="MainContent_NotesView_ctl03_NoteTextTextBox" />
<input type="image" name="ctl00$MainContent$NotesView$ctl03$AcceptNoteButton" id="MainContent_NotesView_ctl03_AcceptNoteButton" data-objectid="57101f10-5aed-4285-99e7-2709c3ac41e9" src="../Resources/Icons/64x64/circle-check.png" alt="Save" />


Let's say we get our data from the database every time user reloads the page:


protected void Page_Load(object sender, EventArgs e)
{
       List<Note> notes = GetNotesFromDb();
       RenderNotes(notes);
}

private void RenderNotes(List<Note> notes)
{
       Panel notePanel = new Panel();
       Notes.ContentTemplateContainer.Controls.Add(notePanel);

       for (int i = 0; i < notes.Count; i++)
       {
              var note = notes[i];

              // Here is our NoteControl
              NoteControl noteCtrl = (NoteControl)Page.LoadControl("~/Controls/NoteControl.ascx");
              noteCtrl.Note = note;
              notePanel.Controls.Add(noteCtrl);
       }
}

And every NoteControl will have the event handler for the save button:

protected void acceptButton_Click(object sender, ImageClickEventArgs e)
{
       var button = sender as ImageButton;
       Guid id = Guid.Parse(button.Attributes[ObjectIdAttribute]);
       UpdateNoteDb(id);
}

And because we attached this event handler in the markup:

<asp:ImageButton
ID="AcceptNoteButton"
runat="server"
ImageUrl="~/Resources/Icons/64x64/circle-check.png"
AlternateText="Save"
CausesValidation="true"
OnClick="acceptButton_Click" />

This event handler will be *magically* called when you click save button in the browser. Am I wrong?


OK, let's say now we edited some note and now press the save button:


In a meanwhile, while we were editing this note, we opened the same page from the different machine and simply removed this note, and created a new one instead.


So when we hit the save button we get into Page_Load and... get all the notes from the database:

List<Note> notes = GetNotesFromDb();

Without even knowing that we are getting the new notes now.
And then we are rendering our page:

NoteControl noteCtrl = (NoteControl)Page.LoadControl("~/Controls/NoteControl.ascx");
...
notePanel.Controls.Add(noteCtrl);

And surprisingly we get controls rendered with the same ID's:

<input name="ctl00$MainContent$NotesView$ctl03$TitleTextBox" type="text" value="Another one" id="MainContent_NotesView_ctl03_TitleTextBox"/>
<textarea name="ctl00$MainContent$NotesView$ctl03$NoteTextTextBox" id="MainContent_NotesView_ctl03_NoteTextTextBox">Indeed</textarea>
<input type="image" name="ctl00$MainContent$NotesView$ctl03$AcceptNoteButton" id="MainContent_NotesView_ctl03_AcceptNoteButton" data-objectid="78ae8c7f-7236-4a16-950d-b226f710485d" src="../Resources/Icons/64x64/circle-check.png" alt="Save" />

But before they will be rendered, all the events must fire... And *magically* we still handle the acceptButton_Click event. On the newly created control, which hasn't even been on the client yet. 
Non-magically, data-objectid will be object id of the new note.

TitleTextBox.Text and NoteTextTextBox.Text will still get old values, because we set them only on initial page load:

if (!Page.IsPostBack)
{
       TitleTextBox.Text = ...;
       NoteTextTextBox.Text = ...;
}

What kind of makes sense, since these values will be submitted as parameters and extracted from the Request object automatically every time the page will be in postback.

Short story: we update the wrong object.

What can I say... You have to learn or you'll be the monkey which can code anything, if only the requirements are clear.

Sunday, 12 June 2011

Step 2

Move all the JavaScript code to separate files. So now it will be:

<div id="CategoriesViewContainer" class="categoriesViewContainer">
    <div id="CategoriesViewInner" class="categoriesViewInner">
...     
         <input type="image" name="ctl00$MainContent$Categories$ctl03" class="functionButton deleteCategoryButton" OBJECTID="6c45226c-0152-4e12-826e-4d3733c241cb" src="../Resources/Icons/64x64/_blank.png" alt="Del" onclick="return confirm(&#39;Are you sure want to delete a category Universe?&#39;);" />
         <input type="submit" name="ctl00$MainContent$Categories$ctl04" value="Universe" class="categoryButton romamaColor0" OBJECTID="6c45226c-0152-4e12-826e-4d3733c241cb" />
         <input type="image" name="ctl00$MainContent$Categories$ctl05" class="functionButton editCategoryButton" OBJECTID="6c45226c-0152-4e12-826e-4d3733c241cb" src="../Resources/Icons/64x64/_blank.png" alt="Edit" />

There is still onclick handler, but it will also go at some point.

What I want is that when the user hovers the category button the control buttons appear, like delete button:

What I did first was this code in my ASP.Net code behind class:

for (int i = 0; i < categories.Count; i++)
{
       var category = categories[i];

       Panel categoryPanel = new Panel();
...
       Categories.ContentTemplateContainer.Controls.Add(categoryPanel);

       var deleteButton = new ImageButton();
       deleteButton.ID = "deleteCategory" + i;
       ...
       categoryPanel.Controls.Add(deleteButton);

       var categoryCtrl = new Button();
...
       categoryPanel.Controls.Add(categoryCtrl);

categoryPanel.Attributes.Add(
"onmouseover", String.Format(@"$get(""deleteCategory{0}"").src=""../Resources/Icons/64x64/circle-delete.png"" ", i));
categoryPanel.Attributes.Add(
"onmouseout", String.Format(@"$get(""deleteCategory{0}"").src=""../Resources/Icons/64x64/_blank.png"" ", i));
}

And as a result, this was added to my page:

<div onmouseover=" $get(&quot;deleteCategory0&quot;).src=&quot;../Resources/Icons/64x64/circle-delete.png&quot; "
onmouseout=" $get(&quot;deleteCategory0&quot;).src=&quot;../Resources/Icons/64x64/_blank.png&quot; ">

Quite a simple thing.


Now what I had to do in pure JavaScript added in the separate file:

1. Handle onload event.

addEvent(window, "load", initialize);

this required writing this addEvent function:

function addEvent(eTarget, sEvent, fHandler)
{
    // Standard event registration method
    if (eTarget.addEventListener)
    {
        eTarget.addEventListener(sEvent, fHandler, false);
    }
    // IE8 and earlier
    else if (eTarget.attachEvent)
    {
        eTarget.attachEvent("on" + sEvent, fHandler);
    }
}

2. Find all category panels and bind their events to delete buttons:

function bindEventHandlersToCategories() 
{
    if (document.getElementsByClassName)
    {
        var categoryPanels = document.getElementsByClassName("categoryPanel");
        for (var i = 0; i < categoryPanels.length; i++)
        {
            var categoryPanel = categoryPanels[i];
            var deleteButtons = categoryPanel.getElementsByClassName("deleteCategoryButton");

            bindDeleteCategoryButtonsOnCategoryEvents(categoryPanel, deleteButtons);
        }
    }
    // IE 8
    else if (document.querySelectorAll)
    {
        var categoryPanels = document.querySelectorAll(".categoryPanel");
        for (var i = 0; i < categoryPanels.length; i++)
        {
            var categoryPanel = categoryPanels[i];
            var deleteButtons = categoryPanel.querySelectorAll('.deleteCategoryButton')

            bindDeleteCategoryButtonsOnCategoryEvents(categoryPanel, deleteButtons);
        }
    }
    // IE 7 and older
    else 
    {
        var categoryPanels = getElementsByClassName("categoryPanel");
        for (var i = 0; i < categoryPanels.length; i++)
        {
            var categoryPanel = categoryPanels[i];
            var deleteButtons = getElementsByClassName("deleteCategoryButton", categoryPanel);

            bindDeleteCategoryButtonsOnCategoryEvents(categoryPanel, deleteButtons);
        }
    }
}

function bindDeleteCategoryButtonsOnCategoryEvents(categoryPanel, deleteButtons)
{
    for (var i = 0; i < deleteButtons.length; i++)
    {
        addEvent(categoryPanel, "mouseover", function ()
        {
            var button = deleteButtons[i];
            return function ()
            {
                button.src = "../Resources/Icons/64x64/circle-delete.png";
            }
        } ());
        addEvent(categoryPanel, "mouseout", function ()
        {
            var button = deleteButtons[i];
            return function ()
            {
                button.src = "../Resources/Icons/64x64/_blank.png";
            }
        } ());
    }
}

function getElementsByClassName(sClassNamesToFind, eParent)
{
    // Define search scope
    var eStartElement = eParent;
    if (!eStartElement)
    {
        eStartElement = document;
    }

    var aClassNamesToFind = sClassNamesToFind.split(/\s+/);

    var aSelectedElements = new Array();
    for (var i = 0; i < eStartElement.all.length; i++)
    {
        var eElement = eStartElement.all[i];
        var sElementClassName = eElement.className;

        var bInclude = true;
        for (var j = 0; j < aClassNamesToFind.length; j++)
        {
            var oPattern = new RegExp("\\b" + aClassNamesToFind[j] + "\\b");
            if (sElementClassName.search(oPattern) == -1)
            {
                bInclude = false;
            }
        }
        if (bInclude)
        {
            aSelectedElements.push(eElement);
        }
    }
    return (aSelectedElements)
}

Somehow I feel it is too complicated.
Forget about this and start learning jQuery? Is this what I should do?