Saturday, February 13, 2010

Google Chrome Extension: New Tab Redirect! STEP BY STEP

Background
(see http://code.google.com/chrome/extensions/overview.html for a thorough background of Chrome Extension development)
A Google Chrome Extension is a mini web application which runs in its own process and can perform actions at a browser-level or page-level.
An extension consists (or may consist) of a number of parts:
  • manifest.json
  • Web files (html, js, css, images)
  • NPAPI Plugins
  • background.html (the long-running process)
All of these files are bundled into a package with a .crx file extension. When uploading your extension to the Google Chrome Extensions Gallery, you only zip up the files your extension needs and Google will package it properly for you.

      The manifest.json file is a file which specifies properties of your extension in JavaScript Object Notation (aka JSON). For a quick intro to JSON, check out http://www.learn-ajax-tutorial.com/Json.cfm

The Manifest File
manifest.json:

   1:  {
   2:    "name": "New Tab Redirect!",
   3:    "description": "Sets a user-specified URL to load in new tabs.",
   4:    "version": "0.0.1.109",
   5:    "background_page": "background.html",
   6:    "options_page":"options.html",
   7:    "chrome_url_overrides": {
   8:      "newtab": "redirect.html"
   9:    },
  10:      "permissions": ["tabs"],
  11:      "icons": {
  12:      "128": "icon128.png",
  13:      "19":"icon19.png"
  14:    }
  15:  }
(end manifest.json)
The name, description, version, background_page, and options_page key/value pairs are pretty self-explanatory.

The "chrome_url_overrides" section is interesting. As you can see, the value for this entry is itself an object. This object allows me to override "newtab" with my own page, "redirect.html". Currently, the chromium team will only allow you to override newtab, however, I anticipate they'll allow you to override other pages in the future. For a list of url constants used within Chrome, check out the url_constants.cc file in the source code repository. Possible future overrides would be anything that is listed near the end of that file as 'const char kChromeUI*Host[]'

Back to the file. "permissions" takes an array of requested permissions. New Tab Redirect! requires permissions to update tabs. This is because we're not only redirecting to html web-hosted sites; New Tab Redirect allows you to set web pages, local files, or inherent Chrome pages.

"icons" specifies the location of the icons your extension will use. These icons will be displayed in the Gallery and in the Extensions page within Google Chrome

Remember: You must change your version number when updating your publicly hosted extension, or else Chrome won't find a new version and your users will not be updated.

The Background Page
background.html

   1:  <html>
   2:  <head>
   3:  <script type="text/javascript">
   4:  String.prototype.startsWith = function(str){
   5:      return (this.indexOf(str) === 0);
   6:  }
   7:   
   8:  var url = null;
   9:  var newTabId = undefined;
  10:  var protocol =  undefined;
  11:  var allowedProtocols = ["http://","https://","about:","file://", 
  12:    "file:\\", "file:///", "chrome://","chrome-internal://"];
  13:   
  14:  function setUrl(url) {
  15:          if(protocolPasses(url) 
  16:            && url.length > 8) { /* 8 is arbitrary */
  17:              this.url = url;     
  18:          } else {
  19:              protocol = 'http://'; /* force http */
  20:              var right = url.split('://')
  21:              if(right != undefined && right != null && right.length > 1) {
  22:                  this.url = protocol + right[1];
  23:              } else { 
  24:                  /* this will redirect to http:// if url is empty */
  25:                  this.url = protocol + url; }    
  26:          }
  27:          localStorage["url"]  = this.url;
  28:          
  29:      function protocolPasses(url) {
  30:          if(typeof(url) == 'undefined' || url == null) { return false; }
  31:          if (url.startsWith(allowedProtocols[3]) 
  32:              && !url.startsWith(allowedProtocols[5])) {
  33:              url.replace(allowedProtocols[3], allowedProtocols[5]);
  34:          } else if (url.startsWith(allowedProtocols[4])) {
  35:              url.replace(allowedProtocols[4], allowedProtocols[5]);
  36:          }
  37:          
  38:          for(var p in allowedProtocols) {
  39:              if(url.startsWith(allowedProtocols[p])) { return true;}
  40:          }
  41:          return false;
  42:      }
  43:  }
  44:   
  45:  function init() {
  46:     url = localStorage["url"] || "http://www.facebook.com";
  47:  }
  48:   
  49:  function r(tabId) {
  50:      chrome.tabs.update(tabId, {"url":this.url});
  51:  }
  52:   
  53:  chrome.tabs.onUpdated.addListener(function(tabId,info,tab) {
  54:      if (info.status === 'loading')
  55:          console.log('Loading url ... ' + tab.url)
  56:      if(info.status === 'complete')
  57:          console.log('Finished loading url ... ' + tab.url)
  58:  });
  59:   
  60:  chrome.tabs.onCreated.addListener(function(tab) {
  61:      newTabId = tab.id
  62:  });
  63:  </script>
  64:  </head>
  65:  <body onload="init()"></body>
  66:  </html>
(end background.html)
This page is pretty self-explanatory for the most part. The background page is your long-running process, and is usually used to house any variables that are shared between pages. However, this isn't a necessity of Chrome Extension architecture. Every page in an extension can interact with any other page through chrome.extension.getViews() or chrome.extension.getBackgroundPage() . Because of the nature of New Tab Redirect, I've used a background page.

One thing of note is the initialization of the extension. Since the operation is fairly simple, allowing a user to specify a url and then redirecting to that url, the only thing I need to worry about immediately is the url. This is stored using HTML 5's local storage. If the url doesn't exit, I set it to facebook.

The setUrl function is located in the Background page and is called from options.html. It is located here so the options page isn't trying to set the url variable that is maintained by the background page. This function checks my array of allowed protocols and validates the url with some simple rules.

r(tabId) is the function which actually updates the tab when the protocol isn't an http or https protocol. The function name is short because the the code in redirect.html should be as concise as possible. This function calls the chrome.tabs.update method (one culprit behind the tabs permission requirement)

That tabId is provided via the redirect.html, which gets the currently created tab's id. However, again note that this processing only occurs when the url is local.

Options

Because the options page is simple html and styles, I'll only include the JavaScript which sets the url.

options.js

   1:  String.prototype.startsWith = function(str) {
   2:      return (this.indexOf(str)===0);
   3:  }
   4:  var chromePages = {
   5:      Extensions : "chrome://extensions/",
   6:      History : "chrome://history/",
   7:      Downloads : "chrome://downloads/",
   8:      NewTab : "chrome-internal://newtab/"
   9:  }
  10:  var aboutPages = ["about:blank","about:version", "about:plugins","about:cache", 
  11:      "about:memory","about:histograms","about:dns",
  12:      "chrome://extensions/","chrome://history/",
  13:      "chrome://downloads/","chrome-internal://newtab/"];
  14:  var popularPages = { 
  15:      "Facebook":"www.facebook.com",
  16:      "MySpace":"www.myspace.com",
  17:      "Twitter":"www.twitter.com",
  18:      "Digg":"www.digg.com",
  19:      "Delicious":"www.delicious.com",
  20:      "Slashdot":"www.slashdot.org"
  21:  };
  22:   
  23:      
  24:  // save options to localStorage.
  25:  function save_options() {
  26:      var url = $('#custom-url').val();
  27:      if(url == ""){
  28:          url = aboutPages[0];
  29:      }
  30:      
  31:      if( $.inArray(String(url), aboutPages) || isValidURL(url)) {
  32:          save(true,url);
  33:      } else {
  34:          save(false,url);
  35:      }        
  36:  }
  37:   
  38:  function save(good,url) {
  39:      if(good) {
  40:          chrome.extension.getBackgroundPage().setUrl(url);
  41:          $('#status').text("Options Saved.");
  42:      } else {
  43:          $('#status').text( url + " is invalid. Try again (http:// is required)");    
  44:      }
  45:      
  46:      $('#status').css("display", "block");
  47:      setTimeout(function(){
  48:          $('#status').slideUp("fast").css("display", "none");
  49:      }, 1050);
  50:  }
  51:   
  52:  // Restores select box state to saved value from localStorage.
  53:  function restore_options() {
  54:      var url = chrome.extension.getBackgroundPage().url || "http://www.facebook.com/";    
  55:       $('#custom-url').val(url);
  56:  }
  57:   
  58:  function isValidURL(url) {
  59:      var urlRegxp = /^(http:\/\/www.|https:\/\/www.|ftp:\/\/www.|www.){1}([\w]+)(.[\w]+){1,2}$/;
  60:      if (urlRegxp.test(url) != true) {
  61:          return false;
  62:      } else {
  63:          return true;
  64:      }
  65:  }
  66:   
  67:  function saveQuickLink(url){
  68:      var uurl = unescape(url);
  69:       $('#custom-url').val(uurl);
  70:       save(true,uurl);
  71:       return false;
  72:  }
  73:   
  74:  $(document).ready(function(){
  75:      restore_options();
  76:      $.each(chromePages, function(k,v) {
  77:          var anchor = "<a href=\"javascript:saveQuickLink('"+v+"');\">"+k+"</a>";
  78:          $('#chromes').append("<li>" + anchor + "</li>");
  79:      });
  80:      $.each(aboutPages, function() {
  81:          if(this.startsWith("about:")) { /* quick fix to handle chrome pages elsewhere */
  82:              var anchor = "<a href=\"javascript:saveQuickLink('"+this+"');\">"+this+"</a>";
  83:              $('#abouts').append("<li>" + anchor + "</li>");
  84:          }
  85:      });
  86:      $.each(popularPages, function(k,v) {
  87:          var anchor = "<a href=\"javascript:saveQuickLink('"+v+"');\">"+k+"</a>";
  88:          $('#popular').append("<li>" + anchor + "</li>");
  89:      });
  90:  });
(end options.js)

As you can see, I've included jQuery for dom manipulation. I decided to do this because jQuery is so small and because the Options page can be whatever size you'd like (keeping in mind user experience, of course). This saved me some time from writing out a little bit of javascript.

Of note here is how I build the lists of Chrome pages, About pages, and Popular pages. This isn't included in the options.html file, which only has placeholders for the unordered lists. This allows me to go in and provide a page name and url for chromePages and popularPages, or just the url for aboutPages. If I want to add anything in the future, I'll just add a key/value pair (or url) to one of these objects. If I want to remove, likewise, I only touch the respective object.

Quick links (one from an above-mentioned object) are saved automatically, bypassing the url checking regex. However, you'll notice in the save_options function that I check first to see if the url is in the list of about pages, this is because a user will most likely enter about:memory or any valid url, and whould be less likely to enter chrome-internal://newtab/. In the future, I will probably add checks on all objects before checking a valid url, but this kind of thing happens in incremental development.
As you can see, if the user gets to save the url, the function setUrl from the background page is called via chrome.extension.getBackgroundPage().setUrl(url); Following good user interface techniques, we have to let the user know what happened, which is what the next line and the rest of the function accomplishes.

Note: The new tab url is chrome-internal:// instead of chrome://. Why is this, do you think? Don't think too hard, though. It's simple: redirect.html is now chrome://newtab/, because I've told chrome to override the newtab with my own file. You can test it out by typing chrome://newtab into Google Chrome. Try chrome-internal://newtab and see what happens? This can only be called internally, hence the name, and must be called from within an extension via chrome.tabs.update. If you don't use this internal url, when you set the url to chrome://newtab and hit CTRL+T or click the '+' tab, your tab will keep trying to call itself, and eventually stop, after doing nothing meaningful.

Redirect.html

Finally, the beast that does it all.

redirect.html

   1:  <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
   2:  <html>
   3:    <head>
   4:      <meta name="generator" content=
   5:      "HTML Tidy for Windows (vers 14 February 2006), see www.w3.org">
   6:      <title>
   7:        Redirecting...
   8:      </title>
   9:      <script type="text/javascript">
  10:      var wid, tid;
  11:   
  12:      function r() {
  13:          var url = localStorage["url"] || "";
  14:          if (url.toLowerCase().indexOf("http:") === 0 || 
  15:              url.toLowerCase().indexOf("https:") === 0) {
  16:              document.location.href = url;
  17:              return;
  18:          } else {
  19:              chrome.windows.getCurrent(function(w) {
  20:                  wid = w.id;
  21:                  chrome.tabs.getSelected(wid, function(t) {
  22:                      tid = t.id;
  23:                  });
  24:              });
  25:              chrome.extension.getBackgroundPage().r(tid);
  26:          }
  27:      }
  28:  </script>
  29:    </head>
  30:    <body onload="setTimeout('r()',100);r();return false;">
  31:      Redirecting...<br>
  32:      <em>If your page doesn't load within 5 seconds, 
  33:        <a href="javascript:r()">click here</a></em>
  34:    </body>
  35:  </html>
(end redirect.html)

Note: I have run the redirect.html file through HTML Tidy and a JavaScript formatter at http://jsbeautifier.org/, but this file should be as compact as possible. If you were to download New Tab Redirect and look at the source code, you'd see this code is all on one line. I'm writing this for your enjoyment, though, and one-liners aren't fun to look at.
The only thing special about this file is the the onload function. You'll notice how it runs r after 100 ms, then runs it again. There is quite an odd situation here: Chrome either has to initialize localStorage, doesn't allow us to get the current window and tab id immediately, or the processing required to do so takes longer than 100ms. You could, honestly, run the redirect function through a loop until it redirects (considering the document.location.href will immediately redirect the browser tab. If the url specified by the user is local, the function gets the current window, and from that the id of the current tab, and passes that to the background page's function.

I've been asked by a user or two to remove the redirect message, but I'll keep it for now. I consider a message necessary if, for some reason, a future release of Chrome breaks the extension.

The current source code for New Tab Redirect can be accessed via your Chrome Extensions directory after installing the extension, and is availing at the New Tab Redirect Project Page on Google Code.

If you have any questions or comments about this extension, please contact me.

Wednesday, February 10, 2010

Quickly create a Dictionary using Linq to Objects

When working with collections or lists, sometimes you just need to get a simple dictionary.
There are a number of ways to do this, but the ToDictionary method seems promising.
For isntance:

   1:  IList<Person> people = new List<Person>();
   2:  people.Add(new Person 
   3:      {
   4:          FirstName = "Jim", 
   5:          LastName="Schubert", 
   6:          Age=29, 
   7:          Phone="123-333-4444"
   8:      });
   9:  people.Add(new Person 
  10:      {
  11:          FirstName = "Joe", 
  12:          LastName="Schubert", 
  13:          Age=25, 
  14:          Phone="800-999-1111"
  15:      });
  16:      
  17:  Dictionary<int,string> names = people.ToDictionary(k => k.Age, v => v.FirstName);


There may not be many uses for this type of syntax, but simple transformations like this make Linq fun.

Sunday, January 24, 2010

Mixing Business Logic and the Presentation Layer... Why do people do it?

In .NET Application Architecture Guide, 2nd Edition, the writers explicitly say:
Do not mix different types of components in the same logical layer. Start by identifying different areas of concern, and then group components associated with each area of concern into logical layers. For example, the UI layer should not contain business processing components, but instead should contain components used to handle user input and process user requests.
This is the standard way of developing today. However, I've seen time and again where a developer will do ALL of the business logic processing in the presentation layer. More than merely being annoying, this is wrong for a number of reasons including:
  • redundancy in code
  • "scattered" logic
  • poor abstraction
  • data access in the presentation layer
  • strong-coupling
That's just to name a few. Every time I see this coding snafu occur, I think to myself, "Do I just not know what I'm doing?" After all, it's much more seasoned developers doing this. Then, I remind myself that object-oriented programming has changed dramatically over the past 10 years.
I can understand some points of view "Too much abstraction is a bad thing!", "Keep the logic as close to where you're using it as possible!", "I don't understand what you mean by business logic." But, ignorance of norms and coding standards doesn't inherently and automagically produce good, clean, scalable code.
N-tier architecture has now become the norm, but is it too much to ask for some logical layering within a tier?

Tuesday, December 22, 2009

UpdatePanel won't AsycPostback for anything.

I spent a lot of time today and Friday trying to get my UpdatePanel to be handled properly. No matter what I did, my asynchronous triggers were only posting back. I searched and searched, and couldn't find anything. Then, I ran across a post on asp.net's forum which had the answer.
<xhtmlConformance mode="Legacy"/> :
Change Legacy to Strict or Transitional
Don't you love Microsoft?
Other things I searched for in order to find this answer:
"IsInAsyncPostBack false masterpage", "UpdatePanel only postback", "asp:AsyncPostBackTrigger won't work"

Sunday, December 20, 2009

Google Chrome Extension: New Tab Redirect!

Thanks to the blizzard this weekend, I've been alone and stuck inside long enough to write an extension for Google Chrome. It's called "New Tab Redirect!" and it allows you to customize the page displayed when you add a new tab (either with CTRL+T or clicking the (+) in the title bar).

A cool thing about the extension is that it allows you to specify about: pages, URLs, or local files.

The extension is available in the extension Gallery: http://bit.ly/info/8oNcya

Thursday, December 17, 2009

jQuery check/uncheck all Checkboxes in ASP.NET

For some odd reason, ASP.NET GridView wraps html elements in a span element. When you apply a CssClass to the asp:CheckBox for instance, you will have a span with that class and inside you'll have an input with type=checkbox.
So how do you access all of the checkboxes?

   1:              $('.chkAllSelector > input:checkbox').click(function() {
   2:                  $('.chkRowSelector > input:checkbox').each(
   3:                      function(){
   4:                          this.checked = $('.chkAllSelector > input:checkbox').attr('checked');
   5:                      }
   6:                  );
   7:              });
   8:              $('.chkRowSelector > input:checkbox').click(function() {
   9:              if ($('.chkAllSelector > input:checkbox').is(':checked') && !($(this).is(':checked')))
  10:                  { $('.chkAllSelector > input:checkbox').removeAttr('checked'); }
  11:              });

Wednesday, December 16, 2009

Extending IDataReader

I like to access my DataReader objects via column name. Unfortunately, IDataReader supports getting items via integer indexer.

To overcome this *obstacle*, I've written an extension method I'd like to share. It checks to see whether a column exists and has a value associated with it.

   1:      public static class IDataReaderExtension
   2:      {
   3:          /// <summary>
   4:          /// Adds the HasValue property to an IDataReader, which checks based on Column name
   5:          /// </summary>
   6:          /// <param name="reader">An IDataReader object</param>
   7:          /// <param name="column">The string column name</param>
   8:          /// <returns>boolean</returns>
   9:          public static bool HasValue(this IDataReader reader, string column)
  10:          {
  11:              if (reader.GetSchemaTable().Columns.Contains(column))
  12:                  return reader[column] != null || reader[column] != DBNull.Value;
  13:              else
  14:                  return false;
  15:          }
  16:      }

In order to use this method, you can now check:

   1:  if(reader.HasValue("MY_COLUMN"))
   2:  {
   3:      myObject.ColumnInfo = Convert.ToString(reader["MY_COLUMN"]);
   4:  }

Wednesday, November 4, 2009

Microsoft SQL Server's Database Publishing Wizard BatchParser error

http://download.microsoft.com/download/4/4/D/44DBDE61-B385-4FC2-A67D-48053B8F9FAD/SQLServer2005_XMO_x64.msi

Could not load file or assembly 'Microsoft.SqlServer.BatchParser, Version=9.0.242.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' or one of its dependencies. The system cannot find the file specified. Installing the above file will fix this problem on 64-bit machines!

I remember having a hard time finding the answer to this problem about a year ago, and I just ran into it again.

Archive