Monday, February 18, 2013

Work with WinJS ListView Control - The ASP.NET Way

In this post a Windows 8 JavaScript app is created to display a list of phones with sorting and editing functions. The WinJS ListView control is used in the app much like what we do with GridView in typical ASP.NET web application.

Note that this post presents a way to resolve some common problem and demos how we can work wiht Windows 8 WinJS. It doesn't mean that's the recommended way or the best practice.

HTML (home.html)

Create a Windows 8 JavaScript project using Navigation App template (other template should also work), update the home.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>homePage</title>

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

    <script src="/js/jquery-1.8.2.js"></script>
    <link href="/css/default.css" rel="stylesheet" />
    <link href="/pages/home/home.css" rel="stylesheet" />
    <script src="/pages/home/home.js"></script>

</head>
<body>
    <!-- The content that will be loaded and displayed. -->
    <div class="fragment homepage">
        <header aria-label="Header content" role="banner">
            <button class="win-backbutton" aria-label="Back" disabled type="button"></button>
        </header>

        <section aria-label="Main content" role="main">

            <div id="listviewPage">
                <div class="header">
                    <h1>My Smartphone List</h1>
                </div>
                <div class="row title">
                    <span id="titleName">Name</span>
                    <span id="titleBland">Bland</span>
                    <span id="titleOS">OS</span>
                    <span id="titleSize">Size</span>
                    <span id="titlePrice">PriceFrom</span>
                    <span id="titleAdd">Add</span>
                </div>
                <div id="lvPhonesTemplate" data-win-control="WinJS.Binding.Template">
                    <div class="row">
                        <span data-win-bind="innerText: Name"></span>
                        <span data-win-bind="innerText: Bland"></span>
                        <span data-win-bind="innerText: OS"></span>
                        <span data-win-bind="innerText: Size"></span>
                        <span class="price" data-win-bind="innerText: PriceFrom"></span>
                        <span class="edit">Edit</span>
                    </div>
                </div>
                <div class="listview">
                    <div id="lvPhones" data-win-control="WinJS.UI.ListView"
                        data-win-options="{tapBehavior: 'none', selectionMode: 'none', layout: {type: WinJS.UI.ListLayout}}">
                    </div>
                </div>
            </div>

            <div id="editPage">
                <div class="header">
                    <h1><label id="editTitle">Edit</label></h1>
                </div>
                <div class="form">
                    <div class="row">
                        <label for="editName">Name</label><input id="editName" type="text" required="required" />
                    </div>
                    <div class="row">
                        <label for="editBland">Bland</label><input id="editBland" type="text" required="required" />
                    </div>
                    <div class="row">
                        <label for="editOS">OS</label><input id="editOS" type="text" required="required" />
                    </div>
                    <div class="row">
                        <label for="editSize">Size (inch)</label><input id="editSize" type="text" required="required" />
                    </div>
                    <div class="row">
                        <label for="editPrice">Price From</label><input id="editPrice" type="text" required="required" />
                    </div>
                    <div class="error"></div>
                    <div class="footer">
                        <button id="btnCancel">Cancel</button>
                        <button id="btnEdit">Update</button>
                    </div>
                </div>
            </div>
        </section>
    </div>
</body>
</html>

The UI is quite simply. It contains two sections: a page to display tabular data using a ListView and a template for the ListView, and an edit form for editing an existing phone or adding a new phone entry. The jQuery is included so we can manipulate DOM elements the way we deal with traditional web apps.

CSS (home.css)

#listviewPage, #editPage { width: 900px; }
#listviewPage .header, #editPage .header { height: 50px; text-align: center; margin-bottom: 30px; }
#listviewPage .listview { width: 900px; overflow: auto; border: solid; }
#listviewPage .row.title { padding-left: 15px; border-bottom-style: none; }
#listviewPage .row { height: 30px; padding: 5px; border-bottom-style: solid; }
#listviewPage .row span:nth-child(6) { width: 50px; font-weight: bold; }
#listviewPage .row span { display: inline-block; width: 150px; }

#editPage .form { width: 800px; padding: 10px; }
#editPage .row { height: 40px; }
#editPage .row input { width: 400px; }
#editPage .row label { display: inline-block; width: 200px; padding: 10px; text-align: right; }
#editPage .footer { margin: 40px 200px; float: right; }
#editPage .footer button { margin: 10px; }

.homepage section[role=main] { margin-left: 120px; }

JavaScript (home.js)

(function () {
    "use strict";
    WinJS.Binding.optimizeBindingReferences = true;
    WinJS.UI.disableAnimations(); // Use jQuery animation instead

    var editedPhone = null;
    var smartphones = generateSampleData();
    var smartphoneList = new WinJS.Binding.List(smartphones);
    
    function generateSampleData() {
        var blands = ["Apple", "Nokia", "Samsung"];
        var oss = ["iOS", "Windows Phone", "Android"];
        var phones = [
            { ID: 1, Name: "iPhone 4", Bland: blands[0], OS: oss[0], Size: 3.5, PriceFrom: 449 },
            { ID: 2, Name: "iPhone 4S", Bland: blands[0], OS: oss[0], Size: 3.5, PriceFrom: 549 },
            { ID: 3, Name: "iPhone 5", Bland: blands[0], OS: oss[0], Size: 4.0, PriceFrom: 649 },
            { ID: 4, Name: "Lumia 820", Bland: blands[1], OS: oss[1], Size: 4.3, PriceFrom: 399 },
            { ID: 5, Name: "Lumia 900", Bland: blands[1], OS: oss[1], Size: 4.3, PriceFrom: 349 },
            { ID: 6, Name: "Lumia 920", Bland: blands[1], OS: oss[1], Size: 4.5, PriceFrom: 449 },
            { ID: 7, Name: "Galaxy S2", Bland: blands[2], OS: oss[2], Size: 4.3, PriceFrom: 349 },
            { ID: 8, Name: "Galaxy S3", Bland: blands[2], OS: oss[2], Size: 4.8, PriceFrom: 499 },
            { ID: 9, Name: "Galaxy Note", Bland: blands[2], OS: oss[2], Size: 5.3, PriceFrom: 599 },
            { ID: 10, Name: "Galaxy Note2", Bland: blands[2], OS: oss[2], Size: 5.5, PriceFrom: 699 },
        ];
        return phones;
    }

    // Listview item databound event  
    function onItemDataBound(container, itemData) {
        var $price = $(".price", container);
        var price = parseFloat($price.text());
        if (price < 400)
            $price.css("color", "green");
        else if ( price > 600)
            $price.css("color", "red");
    }

    // Listview template function
    function listViewItemTemplateFunction(itemPromise) {
        return itemPromise.then(function (item) {
            var template = document.getElementById("lvPhonesTemplate");
            var container = document.createElement("div");
            template.winControl.render(item.data, container);
            onItemDataBound(container, item.data);
            return container;
        });
    }

    // Display listview page
    function showListviewPage(skipDatabinding) {
        $(editPage).hide();
        if (!skipDatabinding) {
            smartphoneList = new WinJS.Binding.List(smartphones);
            lvPhones.winControl.itemDataSource = smartphoneList.dataSource;
            lvPhones.winControl.itemTemplate = listViewItemTemplateFunction;
        }
        $(listviewPage).fadeIn();
    }

    WinJS.UI.Pages.define("/pages/home/home.html", {
        ready: function (element, options) {
            showListviewPage();
        }
    });
})();

A listViewItemTemplateFunction function is defined for the ListView's itemTemplate property so we have granular control on each ListView item at run-time. The template function gets the template elements defined in home.html and injects them inside a div container, then use template's render function to emit the content. The ListView is fully populated at this point.

How to do more business logic for each item like what we do in GridView's itemDataBound event in ASP.NET? Here we define another onItemDataBound function to simulate such process. In our example we apply a logic to show different price color based on its amount: green if less than $400 and red if greater than $600. In ASP.NET we find the control inside template by its ID, here we locate an HTML element by its CSS class. Later we will set the edit button handler inside the onItemDataBound function.

The ListView screen looks like:

Sorting Implementation

The single-column sorting is implemented by updating the datasource of the ListView:
    var sortors = { Name: "asc", Bland: "asc", OS: "asc", Size: "asc", PriceFrom: "asc" };

    // Sorting event handler
    function sortingChanged(title) {
        try {
            var sorter = sortors[title];
            smartphones.sort(function (first, second) {
                var firstValue = first[title];
                var secondValue = second[title];
                if (typeof firstValue == "string")
                    return sorter == "asc" ?
                        firstValue.localeCompare(secondValue) : secondValue.localeCompare(firstValue);
                else {
                    if (firstValue == secondValue)
                        return 0;
                    else if (firstValue > secondValue)
                        return sorter == "asc" ? 1 : -1;
                    else
                        return sorter == "asc" ? -1 : 1;
                }
            });
            sortors[title] = sorter == "asc" ? "desc" : "asc";
            showListviewPage();
        } catch (e) {
            console.log("sort error: " + e.message);
        }
    }

    WinJS.UI.Pages.define("/pages/home/home.html", {
        ready: function (element, options) {
            // listview sorting
            $(".row.title", listviewPage).children().each(function (index, columnTitle) {
                var titleText = $(columnTitle).text();
                if (titleText != "Add") {
                    $(columnTitle).on("click", function () {
                        sortingChanged(titleText);
                    });
                }
            });
   //...
        }
    });

When the user clicks the header (title) of one column, the header text is passed to sortingChanged function so the sorting function knows which column is to sort. For simply demo purpose the "Add" button is put as the Edit column header which is not sortable, so it's exluded from the sorting event binding.

Add and Edit Interaction

The HTML has already included a simple edit form but the related logic is missing. We need to define the navigation handling between the list view and the edit form:

    // Listview item databound event  
    function onItemDataBound(container, itemData) {
        //...
        var $edit = $(".edit", container);
        $edit.on("click", itemData, showEditPage);
    }
    
 // Display Add/Edit form page
    function showEditPage(event) {
        if (event && event.data && event.data.ID) {
            editTitle.textContent = "Edit Smartphone";
            btnEdit.textContent = "Update";
            editedPhone = event.data;
            editName.value = editedPhone.Name;
            editBland.value = editedPhone.Bland;
            editOS.value = editedPhone.OS;
            editSize.value = editedPhone.Size;
            editPrice.value = editedPhone.PriceFrom;
        } else {
            editedPhone = null;
            editTitle.textContent = "Add A New Smartphone ";
            btnEdit.textContent = "Add";
            $("input", editPage).each(function (index, input) {
                $(input).val("");  // Cleanup all the input textbox
            });
        }
        $(listviewPage).fadeOut(function () {
            $(editPage).fadeIn();
        });
    }

    // Add or update an item
    function updateOrAddPhone() {
        if (editedPhone) { // Update existing phone
            editedPhone.Name = editName.value;
            editedPhone.Bland = editBland.value;
            editedPhone.OS = editOS.value;
            editedPhone.Size = editSize.value;
            editedPhone.PriceFrom = editPrice.value;
        } else { // Add a new phone
            var phone = {
                ID: smartphones.length + 1,
                Name: editName.value,
                Bland: editBland.value,
                OS: editOS.value,
                Size: editSize.value,
                PriceFrom: editPrice.value
            };
            smartphones.push(phone);
        }
        showListviewPage();
    }
    
    WinJS.UI.Pages.define("/pages/home/home.html", {
        ready: function (element, options) {
            titleAdd.addEventListener("click", showEditPage);
            btnEdit.addEventListener("click", updateOrAddPhone);
            btnCancel.addEventListener("click", function () { showListviewPage(true); });
            //...
        }
    });

The add and edit share the same form. We toggle the display between listView and edit form page section when editting or finishing editting and list item. In this demo the code doesn't have validation for the edit form, but in reality we should always implement validation logic for the user input.

Another note is that in order to get input elements inside the ListView control to work, such as textbox and select dropdowns, you need to add "win-interactive" class for those elements:

    <input id="amount" class="win-interactive" type="number" />
The Add and Edit forms' screen-shot:

The Final JavaScript (home.js)

(function () {
    "use strict";
    WinJS.Binding.optimizeBindingReferences = true;
    WinJS.UI.disableAnimations(); // Use jQuery animation instead

    var editedPhone = null;
    var smartphones = generateSampleData();
    var smartphoneList = new WinJS.Binding.List(smartphones);
    var sortors = { Name: "asc", Bland: "asc", OS: "asc", Size: "asc", PriceFrom: "asc" };

    function generateSampleData() {
        var blands = ["Apple", "Nokia", "Samsung"];
        var oss = ["iOS", "Windows Phone", "Android"];
        var phones = [
            { ID: 1, Name: "iPhone 4", Bland: blands[0], OS: oss[0], Size: 3.5, PriceFrom: 449 },
            { ID: 2, Name: "iPhone 4S", Bland: blands[0], OS: oss[0], Size: 3.5, PriceFrom: 549 },
            { ID: 3, Name: "iPhone 5", Bland: blands[0], OS: oss[0], Size: 4.0, PriceFrom: 649 },
            { ID: 4, Name: "Lumia 820", Bland: blands[1], OS: oss[1], Size: 4.3, PriceFrom: 399 },
            { ID: 5, Name: "Lumia 900", Bland: blands[1], OS: oss[1], Size: 4.3, PriceFrom: 349 },
            { ID: 6, Name: "Lumia 920", Bland: blands[1], OS: oss[1], Size: 4.5, PriceFrom: 449 },
            { ID: 7, Name: "Galaxy S2", Bland: blands[2], OS: oss[2], Size: 4.3, PriceFrom: 349 },
            { ID: 8, Name: "Galaxy S3", Bland: blands[2], OS: oss[2], Size: 4.8, PriceFrom: 499 },
            { ID: 9, Name: "Galaxy Note", Bland: blands[2], OS: oss[2], Size: 5.3, PriceFrom: 599 },
            { ID: 10, Name: "Galaxy Note2", Bland: blands[2], OS: oss[2], Size: 5.5, PriceFrom: 699 },
        ];
        return phones;
    }

    // Listview item databound event  
    function onItemDataBound(container, itemData) {
        var $price = $(".price", container);
        var price = parseFloat($price.text());
        if (price < 400)
            $price.css("color", "green");
        else if ( price > 600)
            $price.css("color", "red");
        var $edit = $(".edit", container);
        $edit.on("click", itemData, showEditPage);
    }

    // Listview template function
    function listViewItemTemplateFunction(itemPromise) {
        return itemPromise.then(function (item) {
            var template = document.getElementById("lvPhonesTemplate");
            var container = document.createElement("div");
            template.winControl.render(item.data, container);
            onItemDataBound(container, item.data);
            return container;
        });
    }

    // Display listview page
    function showListviewPage(skipDatabinding) {
        $(editPage).hide();
        if (!skipDatabinding) {
            smartphoneList = new WinJS.Binding.List(smartphones);
            lvPhones.winControl.itemDataSource = smartphoneList.dataSource;
            lvPhones.winControl.itemTemplate = listViewItemTemplateFunction;
        }
        $(listviewPage).fadeIn();
    }

    // Display Add/Edit form page
    function showEditPage(event) {
        if (event && event.data && event.data.ID) {
            editTitle.textContent = "Edit Smartphone";
            btnEdit.textContent = "Update";
            editedPhone = event.data;
            editName.value = editedPhone.Name;
            editBland.value = editedPhone.Bland;
            editOS.value = editedPhone.OS;
            editSize.value = editedPhone.Size;
            editPrice.value = editedPhone.PriceFrom;
        } else {
            editedPhone = null;
            editTitle.textContent = "Add A New Smartphone ";
            btnEdit.textContent = "Add";
            $("input", editPage).each(function (index, input) {
                $(input).val("");  // Cleanup all the input textbox
            });
        }
        $(listviewPage).fadeOut(function () {
            $(editPage).fadeIn();
        });
    }

    // Add or update an item
    function updateOrAddPhone() {
        if (editedPhone) { // Update existing phone
            editedPhone.Name = editName.value;
            editedPhone.Bland = editBland.value;
            editedPhone.OS = editOS.value;
            editedPhone.Size = editSize.value;
            editedPhone.PriceFrom = editPrice.value;
        } else { // Add a new phone
            var phone = {
                ID: smartphones.length + 1,
                Name: editName.value,
                Bland: editBland.value,
                OS: editOS.value,
                Size: editSize.value,
                PriceFrom: editPrice.value
            };
            smartphones.push(phone);
        }
        showListviewPage();
    }

    // Sorting event handler
    function sortingChanged(title) {
        try {
            var sorter = sortors[title];
            smartphones.sort(function (first, second) {
                var firstValue = first[title];
                var secondValue = second[title];
                if (typeof firstValue == "string")
                    return sorter == "asc" ?
                        firstValue.localeCompare(secondValue) : secondValue.localeCompare(firstValue);
                else {
                    if (firstValue == secondValue)
                        return 0;
                    else if (firstValue > secondValue)
                        return sorter == "asc" ? 1 : -1;
                    else
                        return sorter == "asc" ? -1 : 1;
                }
            });
            sortors[title] = sorter == "asc" ? "desc" : "asc";
            showListviewPage();
        } catch (e) {
            console.log("sort error: " + e.message);
        }
    }

    WinJS.UI.Pages.define("/pages/home/home.html", {
        ready: function (element, options) {
            // Edit form buttons event handler
            titleAdd.addEventListener("click", showEditPage);
            btnEdit.addEventListener("click", updateOrAddPhone);
            btnCancel.addEventListener("click", function () { showListviewPage(true); });

            // listview sorting
            $(".row.title", listviewPage).children().each(function (index, columnTitle) {
                var titleText = $(columnTitle).text();
                if (titleText != "Add") {
                    $(columnTitle).on("click", function () {
                        sortingChanged(titleText);
                    });
                }
            });

            showListviewPage(); // Show ListView by default
        }
    });

})();

Saturday, February 16, 2013

Android Thread Handling in Configuration Change

The Activity Recreating Issue During Configuration Change

When configuration change occurs in an Android device, e.g. rotating the screen from landscape to portrait mode, the Activity will be destroyed and recreated. This could introduce some issues if some tasks inside Activity are not completed during the configuration change. For instance a worker thread may still be running in background and leaking the memory during the configuration change. We assume that the worker thread here in discussion ties to Activity and will communicate back to Activity instance when it completes its task.

Let's take a look at following Activity code:

public class MainActivity extends Activity {
    private static final String TAG = MainActivity.class.getSimpleName();
    private static int instanceCount = 0;
    private Handler handler;
    private Thread thread;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        instanceCount++;
        Log.d(TAG, "onCreate()");

        textView = (TextView)findViewById(R.id.textView1);
        textView.setText("Activity Instance " + String.valueOf(instanceCount));
            
        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                Log.d(TAG, "Handler thread - " + getThreadInfo());
            }
        };

        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "Worker thread - " + getThreadInfo());
                try {
                    int count = 10;
                    while(count-- > 0) { // pause 10 seconds
                        Thread.sleep(1000); 
                    }
                    Log.d(TAG, "Worker thread sendMmessage to handler");
                    handler.sendEmptyMessage(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }    
    
    @Override
    protected void onDestroy() {
        Log.d(TAG, "onDestroy()");
        super.onDestroy();
    }

    private static String getThreadInfo()
    {
        Thread currentThread = Thread.currentThread();
        String info = String.format("%1$s ID: %2$d Priority: %3$s",  
                currentThread.getName(), currentThread.getId(), currentThread.getPriority());
        return info;
    }
}

A separate thread sleeps for 10 seconds to simulate a long-run task, then updates a UI view by a handler. If the the screen is rotated within 10 seconds, the activity will be recreated, so as a new thread and a new handler. However the old thread is still running in background, consuming resource and leaking the memory. The old Activity object will not be garbage collected at the time of destroy since the handler and thread are referencing it. The view switches from "Activity Instance 1" to "Activity Instance 2", and the LogCat shows:

Disabling Dangling Thread

The easiest method to resolve the issue is set a flag when the activity is destroyed to control the stale thread:

public class MainActivity extends Activity {
    private boolean stopThread = false;
    //...
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    //...
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "Worker thread - " + getThreadInfo());
                try {
                    int count = 10;
                    while(count-- > 0 && !stopThread) { // pause 10 seconds
                        Thread.sleep(1000); 
                    }
                    if (!stopThread) {
                        Log.d(TAG, "Worker thread sendMmessage to handler");
                        handler.sendEmptyMessage(0);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        }
    }
    
    @Override
    protected void onDestroy() {
        Log.d(TAG, "onDestroy()");
        super.onDestroy();
        stopThread = true;
        handler.removeCallbacksAndMessages(null);
    }
    
    //...
 }
The LogCat logs:

Now the first worker thread is cancelled along with its partially completed task. To save the work by first thread, we can use onSaveInstanceState() callback to store the partial result, so later the second worker thread can use it as an initial start point, as described in this post.

Using Static Thread Object

The solution above is not perfect: multiple thread instances created during configuration change which is inefficient and expensive. We can use static thread variable to maintain one thread instance:

public class MainActivity extends Activity {
    private static final String TAG = MainActivity.class.getSimpleName();
    private static int instanceCount = 0;
    private static WorkerThread thread;
    private TextView textView;
    
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "Handler thread - " + getThreadInfo());
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        instanceCount++;
        Log.d(TAG, "onCreate()");

        textView = (TextView)findViewById(R.id.textView1);
        textView.setText("Activity Instance " + String.valueOf(instanceCount));
        
        if (savedInstanceState != null && thread != null && thread.isAlive()) {
            thread.setHandler(handler);
        } else {
            thread = new WorkerThread(handler);
            thread.start();
        }
    }    
    
    @Override
    protected void onDestroy() {
        Log.d(TAG, "onDestroy()");
        super.onDestroy();
        handler.removeCallbacksAndMessages(null);
        if (thread.isAlive()) {
            thread.setHandler(null);
        }
    }

    private static String getThreadInfo()
    {
        Thread currentThread = Thread.currentThread();
        String info = String.format("%1$s ID: %2$d Priority: %3$s",  
                currentThread.getName(), currentThread.getId(), currentThread.getPriority());
        return info;
    }
    
    private static class WorkerThread extends Thread {
        private Handler handler;

        public WorkerThread(Handler handler) {
            super();
            this.handler = handler;
        }

        public void setHandler(Handler handler) {
            this.handler = handler;
        }

        @Override
        public void run() {
            Log.d(TAG, "Worker thread - " + getThreadInfo());
            try {
                int count = 10;
                while (count-- > 0) { // pause 10 seconds
                    Thread.sleep(1000);
                }
                if (handler != null) {
                    Log.d(TAG, "Worker thread sendMmessage to handler");
                    handler.sendEmptyMessage(0);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Notice the extended WorkerThread class is also static to avoid memory leak, as in Java non-static inner and anonymous classes will implicitly hold an reference to their outer class. Now the LogCat logs:

As a side note, be cautious to use static variables within Activity to avoid memory leak. If you have to use the static variables, do not forget to cleanup the resources/references in the Activity.onDestroy() callback.

Using Fragment to Retain Thread

Another option, also the recommended way from Android Developer Guild, is to use Fragment with RetainInstance set to true to retain one instance of thread. The worker thread is wrapped into the non-UI Fragment:

public class ThreadFragment extends Fragment {
      private static final String TAG = ThreadFragment.class.getSimpleName();
      private Handler handler;
      private Thread thread;
      private boolean stopThread;

      public ThreadFragment(Handler handler) {
          this.handler = handler;
      }
      
      public void setHandler(Handler handler) {
          this.handler = handler;
      }
      
      @Override
      public void onCreate(Bundle savedInstanceState) { 
        Log.d(TAG, "onCreate()");
        super.onCreate(savedInstanceState);

        setRetainInstance(true); // retain one Fragment instance in configuration change
        
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "Worker thread - " + MainActivity.getThreadInfo());
                try {
                    int count = 10;
                    while(count-- > 0 && !stopThread) { // pause 10 seconds
                        Thread.sleep(1000); 
                    }
                    if (handler != null) {
                        Log.d(TAG, "Worker thread sendMmessage to handler");
                        handler.sendEmptyMessage(0);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
      }

      @Override
      public void onDestroy() {
        Log.d(TAG, "onDestroy()");
        super.onDestroy();
        handler = null;
        stopThread = true;
      }
 }

The Fragment feature was added from Android 3.0 Honeycomb. For older versions you need to include the Android Support package (android-support-v4.jar) to get the Fragment work. With Fragment setup, the main activity will dynamically create or activate existence of ThreadFragment:

public class MainActivity extends Activity {
    private static final String TAG = MainActivity.class.getSimpleName();
    private static int instanceCount = 0;
    private ThreadFragment fragment;
    private TextView textView;
    
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "Handler thread - " + getThreadInfo());
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        instanceCount++;
        Log.d(TAG, "onCreate()");

        textView = (TextView)findViewById(R.id.textView1);
        textView.setText("Activity Instance " + String.valueOf(instanceCount));
        
        FragmentManager fm = getFragmentManager();
        fragment = (ThreadFragment) fm.findFragmentByTag("thread");

        if (fragment == null) {
            fragment = new ThreadFragment(handler);
            fm.beginTransaction().add(fragment, "thread").commit();
        } else { // retained across configuration changes
            fragment.setHandler(handler);
        }
    }    
    
    @Override
    protected void onDestroy() {
        Log.d(TAG, "onDestroy()");
        super.onDestroy();

        fragment.setHandler(handler);
        handler.removeCallbacksAndMessages(null);
    }

    public static String getThreadInfo()
    {
        Thread currentThread = Thread.currentThread();
        String info = String.format("%1$s ID: %2$d Priority: %3$s",  
                currentThread.getName(), currentThread.getId(), currentThread.getPriority());
        return info;
    }
}

The LogCat result:

Thread Safety

The code snippets demoed about are not thread-safe. To make the code thread-safe, we can set the variable as volatile and wrap the setting inside a synchronized method so that only one thread updates the values at any time:

public class ThreadFragment extends Fragment {
    //...
    private volatile Handler handler;
    private volatile boolean stopThread;  
    //...
    
    public void setHandler(Handler handler) {
        synchronized( this.handler ) {
            this.handler = handler;
        }
        if (handler == null){
            requestStop();
        }
    }      

    public synchronized void requestStop() {
        stopThread = true;
    }
    
    thread = new Thread(new Runnable() {
        @Override
        public void run() {
           //...
           synchronized (handler) {
                if (handler != null) {
                    //...
                    handler.sendEmptyMessage(0);
                }
            }
        }
    }
      
    @Override
    public void onDestroy() {
        //...
        requestStop();
        handler = null;
    }
    
    //...
}

Above code avoids the scenarios like the work thread goes into the block after checking handler is not null, but right at that moment the handler is set to null by the main thread. However I am not so sure if such implementation is necessary. Unlike server side services that may be invoked by multiple callers at the same time, Android apps run locally so this kind of race conditions would rarely occur.

Monday, February 04, 2013

WinJS Unhandled Exceptions and Error Messages

In Windows 8 store app unhandled exceptions can be caught by WinJS.Application.onerror event handler. The app will terminate if such onerror handler is not defined, or the onerror handler returns false. Handling those unexpected errors to avoid app crash is considered a good practice.

The code below examines the detail error message of unhandled exceptions:

    function error1() {
        var test1 = undefefinedObejct.name;
    }

    function error2() {
        throw 'error from error2';
    }

    function error3() {
        throw new WinJS.ErrorFromName('error2', 'error from error3');
    }

    function error4() {
        WinJS.Promise.as().then(function () { throw 'error from error4'; });
    }
    
    // unhandled exception caught in application onerror event:
    WinJS.Application.onerror = function (error) {
        console.log(error);
        return true; // app terminates if false
    }
    
    function errorTest() {
        //error1.type = "error",
        //error1.detail.errorLine = 12,
        //error1.detail.errorMessage = "'undefefinedObejct' is undefined",
        //error1.detail.Url = "ms-appx://errortest.js/js/default.js',
        error1();
        
        //error2.type = "error",
        //error2.detail.errorLine = 16,
        //error2.detail.errorMessage = "error from error2",
        //error2.detail.Url = "ms-appx://errortest.js/js/default.js',
        error2();
        
        //error3.type = "error",
        //error3.detail.errorLine = 20,
        //error3.detail.errorMessage = "error2: error from error3",
        //error3.detail.Url = "ms-appx://errortest.js/js/default.js',
        error3();
        
        //error4.type = "error",
        //error4.detail.exception = "",
        //error4.detail.promise = {promise object},
        error4();
    }
We can see that the error message thrown from WinJS promise is very different from the regular JavaScript code. WinJS.Promise.timeout function has two major usage:
  • WinJS.Promise.timeout(100).then(function () {}): pause 100 milliseconds then continue the next promise function.
  • WinJS.Promise.timeout(100, anotherPromise).then(function () {}): start a timer, if anotherPromise is completed within 100 millisconds, then next promise will continue to run, otherwise an error will be thrown with a 'Canceled' message.
Following code snippet tests the WinJS.Promise.timeout function:
      function dotimeTests() {
        timeoutTest(100, 1000).done(function (data) {
            var result = data; // data = 'result from timeoutTest(100,1000)
        });
        timeoutTest(1000, 100).done(function (data) {
            var result = data; // data.name = 'Canceled', data.message = 'Canceled', data.description = 'Canceled'
        });
        timeoutExceptionTest(100, 1000).done(function (data) {
            var result = data; // data = 'error from timeoutExceptionTest(100, 1000)'
        });
        timeoutExceptionTest(1000, 100).done(function (data) {
            var result = data; // data.name = 'Canceled', data.message = 'Canceled', data.description = 'Canceled'
        });
    }

    function timeoutTest(actionTime, timeout) {
        return new WinJS.Promise(function (c, e) {
            // simulate an async call that takes actionTime
            var promiseAction = WinJS.Promise.timeout(actionTime).then(function (data) {
                return 'result from timeoutTest(' + actionTime + ',' + timeout + ')';
            });
            WinJS.Promise.timeout(timeout, promiseAction).done(function (result) {
                c(result);
            }, function (error) {
                c(error);
            });
        });
    }