Monday, April 2, 2012

Adventures in Windows 8: Sticky Notes Metro App

Today I decided it was about time I roll up my sleeves and write my first real Windows 8 'Metro' application--something more substantial than just 'Hello, Metro'. After taking in Dan Wahlin's Metro/HTML5/JavaScript session at DevConnections last week, I figured I was ready. What I created was a Sticky Notes application with persistent storage. You can develop Metro apps 3 ways (C++/XAML, .NET/XAML, or HTML5/JavaScript); since I'm spending a lot of time in HTML5 and JavaScript these days I decided to go down that path.



Inspiration

First I needed an application idea. A sticky notes app is simple enough to do and there are plenty of online web examples of that to use for inspiration. For my Metro app, I would provide the functionality to create, edit, drag and delete notes, storing them with persistence.

I found a good Sticky Notes web example from Design Shack (thank you) and derived from its HTML, CSS and JavaScript in building this application. I also made some improvements such as multiple colors for notes. This sample was a good starting point because the HTML, CSS and JavaScript code is nice and small. It also has drag functionality, provided by the mootools JS library.


Solution Foundation

To create a foundation for the Metro app, I used Visual Studio 11 on a netbook I had received from the Microsoft Professional Developer Conference in 2009. I do have one of the Windows 8 slates from last year's BUILD conference as well, but I find development to be much easier on the netbook--which has a touch screen. Installing Windows 8 on it was easy and problem-free.

I started the solution in Visual Studio 11 with File > New and selected the JavaScript / Windows Metro Style / Blank Application  template.


After adding mootools manually (I needed a deprecated version for the web sample I adapted, so could not use the VS11 library package manager), this is the solution structure I ended up with.



HTML5 Surprises

Although you can leverage your HTML5/CSS/JavaScript skills to develop for Metro, that doesn't not mean everything is the same. Although most things I did just worked, I was quite surprised by some of the operations that turned out to be disallowed.
One area disallowed is links to external executable resources. The original web sample I borrowed from had a link to a Google Web Font, but Windows 8 disallowed this as insecure - so I had to settle for setting font family to "Cursive" and letting the system select a font.

I was also surprised to find I could not set the InnerHTML property of my DOM elements, again a security violation in Windows 8's eyes. I really needed to do this, though, in order to add newly created notes to the DOM. To make this work I had to use a setInnerHTMlUnsafe WinJS method.

    var newHTML = '<div id="N' + nextNoteId.toString() + '" class="stickyNote color' + nextColor.toString() + '" onfocus="javascript:notefocus(' + "'" + nextNoteId.toString() + "')" + '"><h1 id="NH' + nextNoteId.toString() + '" contenteditable="true">' + headingText + '</h1><p id="NT' + nextNoteId.toString() + '" contenteditable="true">' + noteText + '</p></div>';
    var oldHTML = document.getElementById('container').innerHTML;
    WinJS.Utilities.setInnerHTMLUnsafe(document.getElementById('container'), oldHTML + newHTML);
 

HTML

Aside from the aforementioned issues, the HTML went smoothly and you can see it below. The container for the notes is just a DIV, and the notes are child DIVs added dynamically.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Sticky Notes</title>

    <script src="/Scripts/mootools-1.2.5-with-1.1-classes.js"></script>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.0.6/css/ui-dark.css" rel="stylesheet">
    <script src="//Microsoft.WinJS.0.6/js/base.js"></script>
    <script src="//Microsoft.WinJS.0.6/js/ui.js"></script>

    <!-- StickyNotes references -->
    <link href="/css/default.css" rel="stylesheet">
    <script src="/js/default.js"></script>


<style type="text/css">

</style>
<!--<link  href="http://fonts.googleapis.com/css?family=Reenie+Beanie:regular" rel="stylesheet" type="text/css">--> 
</head>
<body>
    <div>
        <button id="NewNote" onclick="javascript:NewNote_Click()" >New Note</button>
        <button id="DeleteNote" style="display:  none" onclick="javascript:DeleteNote_Click()" >Delete Note 1</button>
    </div>

  <div id="container"> 
    <!-- notes will be inserted here -->
</div>

</body>
</html>

CSS

Below is the CSS for the application. Again, short and simple.

body {
    
}

* {
 margin: 0px;
 padding: 0px;
}
 
body {
 background-image: url(/Images/bgd.jpg); background-repeat: repeat;
 color: black;
}

button {
    margin:  4px;
}
 
#container {
 width: 960px;
}

.stickyNote {
 width: 300px; 
    min-height: 275px;
 background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#EBEB00), to(#C5C500));
 background: -moz-linear-gradient(100% 100% 90deg, #C5C500, #EBEB00);
 padding: 20px 20px 20px 20px;
  -webkit-box-shadow: 0px 10px 30px #000;
  -moz-box-shadow: 0px 10px 30px #000;
}

.color1 { background: yellow; }
.color2 { background: pink; }
.color3 { background: cyan; }
.color4 { background: lightgreen; }

.stickyNote h1 {
 font-size: 2.0em;
 font-family: GoodDogRegular, Helvetica, sans-serif;
}
 
.stickyNote p {
 font-family: cursive, GoodDogRegular, Helvetica, sans-serif;
 font-size: 1.0em;
    line-height: 1.75em;
 margin: 10px 0 10px 0;
 width: 280px;
}

 

JavaScript

Most of the code is in the JavaScript. In the listing below, I've left out the persistence code to discuss separately. These functions are concerned with implementing note creation and deletion.

// Note: portions of this are derived from an online CSS sticky notes web example
// http://designshack.net/articles/css/create-a-moveable-sticky-note-with-mootools-and-css3

var noteCount = 0;
var nextColor = 1;
var nextNoteId = 1;
var noteIdWithFocus = "0";

window.addEvent('domready', function(){
    $$('#container div').each(function(drag){
        new Drag.Move(drag);}); 
}); 


// Create a new note. Increment position automatically.

function NewNote_Click() {
    var headingText = "New Note";
    var noteText = "";
    var newHTML = '<div id="N' + nextNoteId.toString() + '" class="stickyNote color' + nextColor.toString() + '" onfocus="javascript:notefocus(' + "'" + nextNoteId.toString() + "')" + '"><h1 id="NH' + nextNoteId.toString() + '" contenteditable="true">' + headingText + '</h1><p id="NT' + nextNoteId.toString() + '" contenteditable="true">' + noteText + '</p></div>';
    var oldHTML = document.getElementById('container').innerHTML;
    WinJS.Utilities.setInnerHTMLUnsafe(document.getElementById('container'), oldHTML + newHTML);

    $$('#container div').each(function (drag) {
        new Drag.Move(drag);
    });

    var noteElement = document.getElementById('N' + nextNoteId.toString());
    noteElement.style.position = "absolute";
    noteElement.style.top = (100 + (noteCount * 75)).toString() + "px";
    noteElement.style.left = (75 + (noteCount * 75)).toString() + "px";

    notefocus(nextNoteId.toString());
    document.getElementById('NT' + nextNoteId.toString()).focus();

    nextNoteId++;

    nextColor++;
    if (nextColor > 4) { nextColor = 1; }

    noteCount++;
}


// Restore a note. Create a note and set its position, heading text, and note text from parameters.

function RestoreNote(top, left, headingText, noteText) {

    if (top === undefined || top === 0 || top === "" || left === undefined || left === 0 || left === "") {
        NewNote_Click();
    }
    else {
        var newHTML = '<div id="N' + nextNoteId.toString() + '" class="stickyNote color' + nextColor.toString() + '" onfocus="javascript:notefocus(' + "'" + nextNoteId.toString() + "')" + '"><h1 id="NH' + nextNoteId.toString() + '" contenteditable="true">' + headingText + '</h1><p id="NT' + nextNoteId.toString() + '" contenteditable="true">' + noteText + '</p></div>';
        var oldHTML = document.getElementById('container').innerHTML;
        WinJS.Utilities.setInnerHTMLUnsafe(document.getElementById('container'), oldHTML + newHTML);

        $$('#container div').each(function (drag) {
            new Drag.Move(drag);
        });

        var noteElement = document.getElementById('N' + nextNoteId.toString());
        noteElement.style.position = "absolute";
        noteElement.style.top = top;
        noteElement.style.left = left;

        nextNoteId++;

        nextColor++;
        if (nextColor > 4) { nextColor = 1; }

        noteCount++;
    }
}


// Delete the note which last had focus.

function DeleteNote_Click() {
    if (noteIdWithFocus === "0") return;
    var note = document.getElementById("N" + noteIdWithFocus);
    if (note != undefined && note != null) {
        document.getElementById("container").removeChild(note);
        noteCount--;
        noteIdWithFocus = "0";
        document.getElementById("DeleteNote").style.display = "none";
    }
}


// Set the focus to a note based on relative Id and set the caption of the Delete Note button.

function notefocus(noteid) {
    noteIdWithFocus = noteid;
    WinJS.Utilities.setInnerHTMLUnsafe(document.getElementById("DeleteNote"), "Delete Note " + noteid);
    document.getElementById("DeleteNote").style.display = "inline";
}

 

Persistence Code

Metro apps have checkpoint (suspend) and activation events, and its important to store and restore state in these events. Originally, I did this through the WinJS.Application,sessionState object - but I found after some testing, as the name implies, that this storage is not retained past a user session. I then found I could store and retrieve JSON objects easily to file storage. That's what the code below does.

// For an introduction to the Blank template, see the following documentation:
// http://go.microsoft.com/fwlink/?LinkId=232509
(function () {
    "use strict";

    var app = WinJS.Application;
        app.onactivated = function (eventObject) {

        WinJS.Application.local.readText("stickynotes-1.db").then(
            function (data) {
                try {
                    var state = JSON.parse(data);
                    
                    if (state != undefined && state[0].noteCount != undefined) {
                        var notes = state[0].noteCount;
                        for (var n = 0; n < notes; n++) {
                            RestoreNote(state[1].top[n], state[2].left[n], state[3].heading[n], state[4].note[n]);
                        }
                    }
                }
                catch (e) {
                    NewNote_Click();
                }
            });

        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
            if (eventObject.detail.previousExecutionState !== Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
                // This application has been newly launched. Initialize 
                // your application here.
            } else {
                // This application has been reactivated from suspension. 
                // Restore application state here.
            }
            WinJS.UI.processAll();
        }
    };

    app.oncheckpoint = function (eventObject) {
        // This application is about to be suspended. Save any state
        // that needs to persist across suspensions here. You might use the 
        // WinJS.Application.sessionState object, which is automatically
        // saved and restored across suspension. If you need to complete an
        // asynchronous operation before your application is suspended, call
        // eventObject.setPromise().
    
        var state = new Object();
        var data = noteCount.toString();
    
        state.noteCount = noteCount;

        state.heading = [];
        state.note = [];
        state.top = [];
        state.left = [];

        for (var n = 0; n < noteCount; n++) {
            var note = document.getElementById("N" + (n + 1).toString());
            var heading = document.getElementById("NH" + (n + 1).toString());
            var noteText = document.getElementById("NT" + (n + 1).toString());
            state.top.push(note.style.top);
            state.left.push(note.style.left);
            state.heading.push(heading.innerText);
            state.note.push(noteText.innerText);
            data = data + "|" + note.style.top + "|" + note.style.left + "|" + heading.innerText + "|" + noteText.innerText;
        }

        var myDataSource = [
            { noteCount: noteCount },
            { top: state.top },
            { left: state.left },
            { heading: state.heading },
            { note: state.note }
        ];
 
        WinJS.Application.local.writeText("stickynotes-1.db", JSON.stringify(myDataSource)).then();
        
    };

    app.start();
})();
 

Icons and Splash Screen

Metro apps have several size logo images and a splash screen image, which by default are black-and-white and rather plan, I created versions of these images for Sticky Notes from screen captures of the application, trimmed to the size of the original icons. Here's how the app appears on the Metro start screen:



Usage

The completed app launches and restores and previously saved notes. If it's being launched for the first time, it creates an initial note.

To create a note, the user clicks the New Note button at top left. To delete a note, select the note and then click the Delete Note button that appears at top left. To drag a note, click and hold a note outside of the editable text area, drag and release.,

To edit a note, the user can click and edit the heading or the body This is done with the HTML5 ContentEditable feature, which allows the HTML elements to be directly editable in the browser without the use of form fields. Although the note shape is nominally square, it will stretch vertically for longer notes.



Summary

The source code to Sticky Notes can be downloaded here.

It took a full day and night to get here--with some frustrations along the way--but I'm pleased with the result of my first Windows 8 Metro application, and I certainly learned a lot. Time to start thinking about what to create next...

No comments: