Saturday, 9 July 2011

Day events: UI prototype


Calendar

Styles applied


It's Friday I'm In Love

I am building a new service, which is going to be Events.
While doing so, I came across some interesting question: how to get the client culture settings?
Because I want to know which day is the first day of the week in my user's world.

Turns out all you have to do is to setup your page so that it gets these settings automatically,


<%@ Page Title="Events" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Events.aspx.cs" Inherits="romama.Calendar.Events" ClientIDMode="Predictable"
    UICulture="auto" Culture="auto" %>

and attaches them to the current thread.

So I set up my calendar so that my first day of week is Tuesday (which is kind of true).


But still, my thread current culture (Thread.CurrentThread.CurrentCulture.DateTimeFormat.FirstDayOfWeek) says it is Monday.

Turns out you don't get the complete culture info from the client, which actually makes sense. Instead, you get the preferred language chosen in the user's browser settings, which is used to determine the culture settings.

Change it to en-US and you'll get Sunday.

Yes, you can see the skeleton of calendar on the background.

Tuesday, 5 July 2011

Search!



So now with the search. Search is performed by tags only, because text is stored encrypted.
Note titles are not encrypted, but they are not considered so far.

Sunday, 3 July 2011

ROMAMA 1.0

I am officially IN THE CLOUD!!!

Romama 1.0 has been released!

Client encryption: final version


function getKey()
{
    var key = localStorage.PBK;
    if (!key)
    {
        var password = prompt("Enter data encryption key. Please do not use your password!");
        if (!password) { return key; }

        var p = {};
        p.iter = 1000;
        p.salt = [0xD1F6D8FF, 0x482648A7];
        key = sjcl.misc.cachedPbkdf2(password, p).key.slice(0, 4);
       
        localStorage.PBK = key;
    }
    return key;
}

function encryptElements()
{
    var key = getKey();
    if (!key || !key.length) return;

    $(".cryptable").each(
        function (idx)
        {
            if ($(this).val() && !$(this).val().match(/\{iv\:".*",salt\:".*",ct\:".*"\}/))
            {
                try
                {
                    $(this).val(sjcl.encrypt(key, $(this).val()));
                }
                catch (e)
                {
                    error("Cannot encrypt: " + e);
                    return false;
                }
            }
        }
    );
    return true;
}

function decryptElements()
{
    var key = getKey();
    if (!key || !key.length) return;

    $(".cryptable").each(
        function (idx)
        {
            if ($(this).val() && $(this).val().match(/\{iv\:".*",salt\:".*",ct\:".*"\}/))
            {
                try
                {
                    $(this).val(sjcl.decrypt(key, $(this).val()));
                }
                catch (e)
                {
                    // could not decrypt, data will be shown as is.
                }
            }
        }
    );
}


It is not totally safe because anyone who has an access to the local machine can obtain the key from the local storage, but it satisfies my needs: as a provider of a service, I will not be able to see the user data, even myself.

Saturday, 2 July 2011

There is your problem

So, after 2 days of struggling with Chrome, I finally figured out the reason why my client encryption didn't work there.

This is the simple page which shows the issue:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
    <title>Test validate</title>
    <script type="text/javascript">
    //<![CDATA[
        function validate()
        {
            var t = document.getElementsByTagName("textarea");
            for (var i = 0; i < t.length; i++)
            {
                alert("Text: " + t[i].innerHTML);
            }
            return false;
        }
    //]]>
    </script>
</head>
<body>
    <form method="post" id="a" action="test.htm" onsubmit="return validate();">
        <textarea type="text" name="myTextArea" rows="2" cols="20" id="myTextArea">Some text</textarea>
        <input type="submit" name="myButton" value="Submit" id="myButton" />
    </form>
</body>
</html>

We have a form with textarea and we want to do something with the textarea modified text before the form is submitted to the server. This can be validation, or encryption, as in my case, or anything else.

The script from the example works perfectly fine in IE8. InnerHTML returns the actual up-to-date text, reflecting all the changes user made before pressing "Submit" button. So does InnerText, textContent and $(this).text().

However, what I get in Chrome is always the text as it was when the form was initially loaded. Changes are simply not there. Neither they can be obtained through InnerText, textContent or $(this).text().

Surprisingly, if I use technically non-existing "value" property, I do get the latest changes, as in IE, so in Chrome.

Using "value" property seem to solve the problem, at least for IE8 and Chrome, except I cannot use jQuery selector anymore, because object returned by $(this) does not have this property.

Client encryption, new version:


function encryptElements()
{
    var cryptable = getElementsByClassName("cryptable");
    var len = cryptable.length;
    for (var i = 0; i < len; i++)
    {
        cryptable[i].value = sjcl.encrypt("password", cryptable[i].value);
    }
    return true;
}


function decryptElements()
{
    $(".cryptable").each(
        function (idx)
        {
            if ($(this).text())
            {
                $(this).text(sjcl.decrypt("password", $(this).text()));
            }
        }
    );
}


decryptElements();

Now I need to decide where to store the password.


UPD: Thanks to my smarter colleague, I figured out I can use $(this).val() and achieve the result I need.
So now it look like that:


function encryptElements()
{
    var key = getKey();
    if (!key || !key.length) return;

    $(".cryptable").each(
        function (idx)
        {
            if ($(this).val() && !$(this).val().match(/\{iv\:".*",salt\:".*",ct\:".*"\}/))
            {
                try
                {
                    $(this).val(sjcl.encrypt(key, $(this).val()));
                }
                catch (e)
                {
                    error("Cannot encrypt: " + e);
                    return false;
                }
            }
        }
    );
    return true;
}

Friday, 1 July 2011

Adding encryption

I know I don't want to trust myself to keep anyone else's secrets, so I want the data to be encrypted in a browser.

I found out quite pretty javascript crypto library: Stanford Javascript Crypto Library

And as a proof of concept, I could get this working in IE.

function encryptElement(eTarget)
{
    $(eTarget).text(sjcl.encrypt("password", $(eTarget).text()));
    $(eTarget).addClass("enc");
}


function decryptElements()
{
    $(".enc").each(
        function (idx)
        {
            if ($(this).text())
            {
                $(this).text(sjcl.decrypt("password", $(this).text()));
                $(this).removeClass("enc");
            }
        }
    );
}


decryptElements();

However, everything breaks in Chrome, because, despite the encryptElement function is called, the data is submitted as it was before the encryption. Also, ScriptManager.RegisterStartupScript, doesn't seem to work as expected in Chrome, so I never get my decryptElements function called after the UpdatePanel is updated.

I am pretty tired of all these browser quirks. The client scripting is so fragile it really freaks me out.

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.