Tuesday, October 25, 2011

Using Siri For Home Automation

I've created a Twilio, Node.js mash up that allows me to control appliances and the thermostat in my house using the iPhone 4S's Siri voice recognition.

Here's a demo of turning on my bedroom fan.



How does this work? From Siri to the end result the chain goes:
  1. Siri 
  2. Twilio SMS number
  3. Node web application
  4. Indigo Web Server
  5. Insteon thermostat/appliance

Let's go through the setup in reverse order in more detail.


Insteon and Indigo

I purchased the following items last year from Amazon to add some basic home automation to my house.

I have a Mac Mini I use as a server which is connected to the PowerLinc Modem via USB. The Indigo software communicates two-way with the Insteon devices in my home via the PowerLinc. In addition to a native iPhone app, the Indigo software has both a web interface and RESTful Api you can use to control your devices.


Node.js Web Application

The Node.js application is the middle man between Twilio and the Indigo web server. When Twilio POSTs the incoming SMS message to the Node app, it parses the message and determines the appropriate Api call to make on the Indigo web server. I wrote some semi-fuzzy logic so the phrases you use don't have to be exact.

For hosting the Node app I picked Heroku because it's convenient to use and free.


Twilio

Setting up Twilio was super easy. I created a Twilio account, purchased a phone number for $1/month, and entered the Url of my Node app that receives the incoming SMS messages.


Siri

To make communicating with my Twilio phone number easy I added a contact called "Gladys" (could be anything but I'm a Portal fan) and associated the Twilio number with her.

I can now control my appliances using the following commands:
  • Tell Gladys to set thermostat to 73
  • Tell Gladys to turn off the bedroom fan


I originally wanted to turn this into a public Siri to Url web service, but I question the demand for such a thing considering trying to make this "generic" would take a lot of time. So if you're interested in adding Siri control to your own use case and don't have programming skills, I'm available for hire and can whip you up something to suite your exact needs. ;)

UPDATE 10/28/2011:
Big thanks to technabob for the coverage! He brought up a good point though, this could easily be faked. Here's a screen shot of my Heroku logs with debugging output on the left and Node.js code for the "fuzzy logic" on the right. Not indisputable evidence but I assure you it's working exactly like it does in the video. ;)

Sunday, October 16, 2011

Windows 7 Registry Entry to Search All File Contents

If you do a search in Windows 7 you may notice that it didn't find some files that you know contain the search terms you are looking for.
This is due to the What to search setting in Windows Explorer under Folder Options -> Search as pictured below.


If you don't have Always search file names and contents selected Windows will only search the files it has indexed.

You can change the setting using the UI as shown above but you can also set it by changing the following Registry setting (which is useful if you want to programmatically set if for users):

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Search\PrimaryProperties\UnindexedLocations]

"SearchOnly"=dword:00000000

Originally I tried to change the following key based on Scott Forsyth's solution for Windows XP and Windows Server 2003 but it didn't work for me on Windows 7:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ContentIndex]

"FilterFilesWithUnknownExtensions"=dword:00000001


As a bonus tip, this is how I found where the Windows 7 registry key was.
  1. Open Registry Editor
  2. Right click the registry hive you suspect the setting to be in (usually either HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE) and select Export and save the .reg file (ex: hklm1.reg)
  3. Change the setting via normal means.
  4. Repeat steps 2 and 3 and save the file with a new name (ex: hklm2.reg).
  5. Use a Diff tool such as the one include with Tortoise SVN or Tortoise Git to search for changes.

Wednesday, September 28, 2011

jQuery Mobile Displaying A Dialog

While working on a mobile web app that is using jQuery Mobile I wanted to display a dialog without making an Ajax call to the server to load it which is the "out of the box" way to do it.

Initially I couldn't figure out how to do this but after some reading and thinking it's ridiculously easy.

You have to use a multi-page template which means you have more then one jQuery Mobile "page" container. Example:
<div data-role="page" data-theme="a">
    <div data-role="header" class="header">
        <a href="#menu" data-icon="grid" data-theme="b" data-iconpos="notext" data-transition="pop">&nbsp;</a>
        <h1>My Web App</h1>
        
    </div>

    <div id="map" data-role="content" data-theme="d">
        <p>This is my main content</p>
    </div>
    
    <div data-role="footer">
        <div>
        by <a href="http://cdeutsch.com" rel="external">CDeutsch</a>
        </div>
    </div>
</div>

<!--start menu-->
<div id="menu" data-role="dialog" data-theme="a" data-url="menu">
    <div data-role="header" class="header">
        <h1>Main Menu</h1>
    </div>
    <div data-role="content" data-theme="d">
        <ul data-role="listview" data-theme="c">
            <li><a href="http://blog.cdeutsch.com" rel="external">My Blog</a></li>
            <li><a href="http://twitter.com/cdeutsch" rel="external">Twitter</a></li>
        </ul>
    </div>
</div>

I'm using a button on the left side of the header to trigger showing the dialog. Just set the href to the id of the dialog (in this case menu) and you're all set. No more repeatedly hitting the server for a frequently used resource.

See this jsfiddle for a working example.

Tuesday, September 6, 2011

Entity Framework Code First Error: Could not create constraint

While using Entity Framework Code First you will run into an error similar to the one below if you create objects that have a circular reference.

Introducing FOREIGN KEY constraint 'File_Folder' on table 'Filess' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.


The fix is pretty simple but not very intuitive.

Add the following line to your OnModelCreating override of your DbContext:

modelBuilder.Entity<File>().HasRequired(oo => oo.Folder).WithMany(oo => oo.Files).WillCascadeOnDelete(false);

So it looks something like this:

public class FilesystemDB : DbContext
{
 public DbSet<File> Files { get; set; }
 public DbSet<Folder> Folders { get; set; }
 
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
 {
  modelBuilder.Entity<File>().HasRequired(oo => oo.Folder).WithMany(oo => oo.Files).WillCascadeOnDelete(false);
 }

}

For the best information on Entity Framework Code First read Morteza Manavi's blog.

Thursday, August 25, 2011

Remove Duplicates From Rdio Playlists

This has driven me nuts ever since I started using Rdio (sign up using this link to support me). I like to listen to other peoples playlists to find new music and if I hear something I like I add the track to my "favorites" playlist. Unfortunately Rdio lets you add the same song multiple times to the same playlist and there isn't an easy way to find and remove duplicates so I end up with Ice Cube's Check Yo Self in my favorites 5 times.

Here's my solution.
1) Install this bookmarklet by dragging it to your bookmarks or right clicking and selecting "add to bookmarks"
2) Browse to the playlist you want to cleanup in Rdio
3) Run the bookmarklet.

If you're interested in other Rdio hacks be sure to checkout my Rdio remote control PartyQ


Saturday, July 30, 2011

OSX Crashes More Then Windows

There! I said it!

For the record, all my computer hardware, phones, and tablets have been Apple for years; but I primarily do programming in .NET which means I still need to use Windows.

I'm on my 3rd Macbook Pro since Sept of 2007 and over that time OSX has consistently crashed more then Windows XP and Windows 7. Up until April of this year, I was primarily booting directly into Windows using Bootcamp and only using OSX 5% of the time and even with 5% use it had more complete lock ups!

Since April I've started running OSX 100% of the time and run Windows 7 off the Bootcamp partition using Parallels.

Since making the switch to 100% of the time I'd estimate OSX does a Black Screen of Death on average 3-4 times per month. I had one waiting for me this morning after leaving the computer on over night.

People think I'm making this up, so I plan to start tweeting every time it crashes for a historical record.

Granted I have a ton of software loaded on OSX, I'm running Parallels, and I use a Diamond BVU195 USB Display Adapter for a second monitor, BUT other then Parallels these are all things I did on Windows as well so I feel it's a fair comparison.

I've had Windows get slow, or weird, or need a reboot. But I can't remember the last Blue Screen of Death I've had, I can usually kill enough process where I can shut down the OS gracefully. I can't say the same for OSX.

I don't plan on switching away from my current setup and I'll be installing Lion soon. I just want people to STFU about how stable OSX is versus Windows, because it's simply not true.

Crash Log
  • 7/30/2011
  • 8/17/2011 (Milestone: first crash of OSX Lion)
  • 9/8/2011
  • 9/19/2011
  • 9/25/2011
  • 9/30/2011
  • 10/14/2011
  • 11/3/2011
  • 5/25/2012
  • 6/22/2012
  • 8/3/2012 (Milestone: first crash of OSX Mountain Lion)
  • 8/9/2012
  • 8/16/2012
  • 8/17/2012
  • 8/17/2012 (second time in one day, grrr)
  • 8/20/2012
  • 8/22/2012
  • 10/14/2012
  • 10/17/2012

Friday, July 15, 2011

ASP.NET MVC3 App Using plupload to Upload Directly to Amazon S3

UPDATE 8/15/2011:
I can't recommend using this for large files (video files). It's very unreliable. Web size image files and normal email attachment sized files would probably do fine. What I ended up doing to get around it was creating a web app on AppHarbor (which runs on EC2) that I upload the files to and it saves them to S3. Since the AppHarbor app is on a different domain you are still limited to Flash and Silverlight support but you do gain file chunking which brings back some reliability. Alternatively to AppHarbor you could setup your own server on Amazon as well.

I created a working example of using plupload to upload files directly to Amazon S3 and threw it on github, since the .NET examples I found on the web we're incomplete or broken.
https://github.com/crdeutsch/MVC3PluploadToAmazonS3

The files that are being uploaded do not even touch your web server. They are posted directly to Amazon. At the moment plupload only supports the Flash and Silverlight plugins as far as I can tell.

The AmazonS3Helper.cs library originated from Cary Abramoff off of this plupload forum thread.

I've made some changed based on iwasrobbed's excellent Ruby on Rails example.

Usage

Upload the crossdomain.xml found in the root of the site to your Amazon S3 Bucket.

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*" secure="false" />
</cross-domain-policy>

Modify the settings in HomeController.cs to use your Amazan credentials and set your S3 bucket.

public ActionResult Index()
{
 string acl = "private";
 string bucket = "YOUR S3 BUCKET NAME";
 string accessKeyId = "YOUR AMAZON ACCESS KEY ID";
 string secret = "YOUR AMAZON SECRET ACCESS KEY";
 string policy = AmazonS3Helper.ConstructPolicy(bucket, DateTime.UtcNow.Add(new TimeSpan(0, 10, 0, 0)), acl, accessKeyId);
 string signature = AmazonS3Helper.CreateSignature(policy, secret);

 var model = new PluploadAmazonS3Model()
 {
  AWSAccessKeyId = accessKeyId,
  Policy = policy,
  Signature = signature,
  Bucket = bucket,
  Acl = acl
 };

 return View(model);
}

On the View side you could just use Razor to dump the Model variables directly into your javascript, but I decided to use hidden form variables instead in case you want to move the javascript to its own file where embedding Razor syntax won't work.

Hope this saves somebody else some time!

Friday, July 1, 2011

ASP.NET & IIS7 Error: Could not load file or assembly 'System.Data.SQLite'

This was annoying. I setup an ASP.NET MVC3 web application on a fresh install of Windows 2008 R2 and got the following error:
Could not load file or assembly 'System.Data.SQLite' or one of its dependencies. An attempt was made to load a program with an incorrect format.

I wasn't even using SQLite in my project; at least that's what I thought. Turns out ELMAH was trying to load it.

The error is due to trying to load the 32bit SQLite.dll on a 64 bit server.

To fix the error set Enable 32-Bit Applications to True for your Application Pool under Advanced Settings.





Sunday, May 1, 2011

Introducing Nodeler

After over a month of work I'm ready to subject my latest work, Nodeler, to the criticisms of the first 50 people who sign up using the Invite Code MakeRocketGoNow. ;)

Here's a video introduction to Nodeler. Otherwise read on...



What is it?
At it's heart I kind of think of it as a higher level, zero configuration service (Apple calls theirs Bonjour).

What good does that do me?
If you're a developer, it will allow you to register your application or game as a "node" that can request to be paired with other "nodes". A node can be anything from a game node requesting to be paired with a controller node to a video web site node requesting a remote control node.

You're not a developer?
Today, there are three Chrome extensions plus a Pacman game you can try.
  1. Nodeler Keyboard: allows you to accept keyboard input on any web page from a remote device.
  2. Nodeler Rdio Remote: allows you to remotely control the Rdio player. Play, pause, mute, and skip forward and back are currently supported.
  3. Nodeler Amazon Video Remote: allows you to remotely control the Amazon Video On Demand player. Play, pause, skip forward and back are currently supported.
  4. Pacman: I've modified Dale Harvey's excellent HTML5 Pacman app to be controllable via a Nodetroller (what I like to call the applications used to provide input to the above Chrome extensions).
You are a developer?
Send me an email if you're interested in using the Nodeler API. Once your Nodes are registered and authenticated via OAuth there are currently just two API calls. Node A submits a PairRequest and then Node B calls GetPairRequests.  The protocol used to communicate between nodes is up to you. If you need a central communication server I hope to offer the Nodeler Central's Node.js server in the near future. This is what the current nodes use. I'm still working out how I want to charge for usage of the Nodeler Central communication server but I do plan to have a free plan.

You're still reading and want to know what technologies this was built with?
Just C# and Javascript. ;)

The Nodeler web site and API are hosted at AppHarbor and were built with:
The server the Nodes communicate through is built using:
The Chrome Extensions use:
The iPhone app that I'm just finishing up was built using:
How can I find out more?
Follow me or Nodeler on Twitter for updates.

Would love to hear feedback on how I can improve the product!


    Tuesday, April 19, 2011

    Using RazorJS with MVC3

    I've been working on a project where I've managed to accumulate 1200+ lines of javascript in my View file. There are two reasons for this:
    1. Since the application is still in development I don't have to worry about the browser caching JS files if the code is in the View.
    2. I often have Url.Action and other snippets of Razor mixed in with my javascript. 
    I was just getting to the point where I was ready to move this code to its own .js file but I was worried about how to deal with the Razor that was mixed in. Thanks to John Katsiotis's (aka djsolid) RazorJS project I now have a solution!

    Installation is easy, just use Nuget to add RazorJS to your solution in Visual Studio. The command line to do is:

    Install-Package RazorJS

    By default @Url.Action will not work and you'll get the error:
    error CS0103: The name 'Url' does not exist in the current context

    To get it to work add the following at the top of your javascript file.
    @{
        var Url = new System.Web.Mvc.UrlHelper(System.Web.HttpContext.Current.Request.RequestContext);
    }
    

    If you run into other errors you should be able to use similar workarounds. If you do use other workarounds, let me know and I can add them to this post.

    UPDATE: In version 0.4.2 you no longer need this work around for the UrlHelper. Thanks to John for adding this so fast!

    Wednesday, April 6, 2011

    Sort Table Rows By Drag and Drop Using jQuery

    jQuery UI allows you to make a list of elements sortable, but if you want to use it to sort table rows you have to do a few workarounds. This is the code I started with.

    $(document).ready(function () {
    
        //make table rows sortable
        $('#myTable tbody').sortable({    
            helper: function (e, ui) {
                ui.children().each(function () {
                    $(this).width($(this).width());
                });
                return ui;
            },
            scroll: true,
            stop: function (event, ui) {
                //SAVE YOUR SORT ORDER                    
            }
        }).disableSelection();
    
    });
    
    


    The helper function (courtesy of The Foliotek Dev Blog) fixes the width of the row which collapses as soon as you remove it from the table.

    This should work for most scenarios, but I've found a bug in Firefox where the row will jump up and float above the cursor when there are scrollbars present on the web page. It seems to be a position issue caused by parent elements of the table. I still haven't figured out exactly what the magic combination is that causes this but I can replicate it with HTML5 Boilerplate when using the default style sheet and table below.

    I've replicated the bug on jsFiddle here:
    http://jsfiddle.net/cdeutsch/2Yxw2/

    <div id="container">
    <header>
    
    </header>
    <div id="main" role="main">
        <div style="position: relative;">
        <table id="myTable">
            <tbody>
            <tr>
                <td>1</td>
                <td>Blah Blah Blah Blah</td>
            </tr>
            <tr>
                <td>2</td>
                <td>2222 22222 22222 2222 22222</td>
            </tr>
            <tr>
                <td>3</td>
                <td>Test Test Test Test Test</td>
            </tr>
            <tr>
                <td>4</td>
                <td>4444 4444 4444 4444 4444</td>
            </tr>
            <tr>
                <td>5</td>
                <td>Hi Hi Hi Hi Hi Hi</td>
            </tr>
            <tr>
                <td>6</td>
                <td>Bye Bye Bye Bye Bye Bye Bye</td>
            </tr>
            </tbody>
        </table>
        </div>
    </div>
    <footer>
    
    </footer>
    </div> <!--! end of #container -->
    
    

    If this happens use the code below (inspired by this Stackoverflow question with some tweaks) to fix the issue.

    Try it in jsFiddle here:
    http://jsfiddle.net/cdeutsch/WysJL/

    $(document).ready(function () {
        //make table rows sortable
        $('#myTable tbody').sortable({
            start: function (event, ui) {
                //fix firefox position issue when dragging.
                if (navigator.userAgent.toLowerCase().match(/firefox/) && ui.helper !== undefined) {
                    ui.helper.css('position', 'absolute').css('margin-top', $(window).scrollTop());
                    //wire up event that changes the margin whenever the window scrolls.
                    $(window).bind('scroll.sortableplaylist', function () {
                        ui.helper.css('position', 'absolute').css('margin-top', $(window).scrollTop());
                    });
                }
            },
            beforeStop: function (event, ui) {
                //undo the firefox fix.
                if (navigator.userAgent.toLowerCase().match(/firefox/) && ui.offset !== undefined) {
                    $(window).unbind('scroll.sortableplaylist');
                    ui.helper.css('margin-top', 0);
                }
            },
            helper: function (e, ui) {
                ui.children().each(function () {
                    $(this).width($(this).width());
                });
                return ui;
            },
            scroll: true,
            stop: function (event, ui) {
                //SAVE YOUR SORT ORDER                    
            }
        }).disableSelection();
    });
    
    

    Hope this helps someone. Took me about 1/2 a day to fix the Firefox issue.

    Saturday, March 26, 2011

    Sensitive Web.config Settings + GitHub + AppHarbor

    So I learned some lessons with having an open source project (WatchedIt.net) on GitHub that is also being deployed to AppHarbor using the same Git repository.

    Number one tip. Do NOT save any sensitive information in any source file that you check into the repository. Even if you make a single commit locally with the sensitive info and then undo it with another commit, do NOT push it to GitHub without removing the history because your history will be sent to GitHub with a normal push. This page on GitHub can help you with that.

    Once we make sure our application does not have any sensitive info we need a way for AppHarbor to know our settings. The easy settings to handle are the Application Settings. AppHarbor has a Configuration Variables page where you define Key/Value pairs in your Web.Config you want AppHarbor to replace. For instance for WatchedIt I store my Twitter Key and Secret tokens like this in the Web.Config that is checked into Git and pushed to GitHub.

    <appSettings>
        <!--Site Settings-->
        <add key="ApplicationName" value="WatchedIt" />
        <add key="Twitter_Consumer_Key" value="YOUR_KEY" />
        <add key="Twitter_Consumer_Secret" value="YOUR_KEY" />
    </appSettings>
    

    Then I configure AppHarbor to replace Twitter_Consumer_Key and Twitter_Consumer_Secret with the real values and everything just works. This is pretty straight forward and covered by AppHarbor here.

    The setting that challenged me though was the connection string. I use Entity Framework Code First running on SQL CE to develop WatchedIt locally. My connection string in the Web.Config I check into Git looks like this:

    <connectionStrings>
        <add name="SiteDB" connectionString="Data Source=|DataDirectory|SiteDB.sdf" providerName="System.Data.SqlServerCe.4.0" />
    </connectionStrings>
    
    

    If you use an AppHarbor SQL or MySQL DB, AppHarbor says it will replace your connection string based on the name property. For some reason this didn't work for me and I think it's because it doesn't change the providerName property. So since AppHarbor also supports Web.Config transformations and even has a nice littler tester for them I decided to combine the two features and see what would happen. To my delight it worked! Here is the code I placed in Web.Release.config to make it work:

    <connectionStrings>
        <add name="SiteDB" connectionString="" providerName="System.Data.SqlClient" xdt:Transform="Replace" />
    </connectionStrings>
    

    Now I don't have to worry about accidently pushing my database connection string to GitHub for the world to see and while developing locally Visual Studio will use my SQL CE DB.

    This is just another example of why I love AppHarbor! I strongly recommend trying them out if you haven't yet!

    ....And one more thing. A big thanks to Michael Friis of AppHarbor for changing by DB password after "the incident". These guys rock at customer service!

    Tuesday, March 22, 2011

    Get Mime Type From File Extension using C#

    I thought this would be built into .NET but I guess it's not. The first way to do this is to use the registry like Kaushik Chakraborti does.

    Since you may have security issues with doing that on a shared hosting provider I decided to take the list of Mime Types I found at FeedForAll (which appears to be link bait but oh well) and make a C# Dictionary out of them.

    This comes in handy if you want to do something like this in MVC:

    public ActionResult Image()
    {
        string filePath = "SOME-FILE.png";
        return base.File(filePath, MimeTypes.GetMimeTypeOrDefault(System.IO.Path.GetExtension(filePath), "binary/octet-stream"));
    }
    
    


    You can find the latest version of this library on GitHub.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    /// <summary>
    /// Dictionary of Mime Types by File Extension.
    /// Created by CDeutsch.
    /// License:
    /// http://creativecommons.org/licenses/by/3.0/
    /// Original list created from here:
    /// http://www.feedforall.com/mime-types.htm
    /// </summary>
    public static class MimeTypes
    {
        public static Dictionary<string, string> MimeTypeDictionary = new Dictionary<string, string> 
        {
            {".ai", "application/postscript"},
            {".aif", "audio/x-aiff"},
            {".aifc", "audio/x-aiff"},
            {".aiff", "audio/x-aiff"},
            {".asc", "text/plain"},
            {".atom", "application/atom+xml"},
            {".au", "audio/basic"},
            {".avi", "video/x-msvideo"},
            {".bcpio", "application/x-bcpio"},
            {".bin", "application/octet-stream"},
            {".bmp", "image/bmp"},
            {".cdf", "application/x-netcdf"},
            {".cgm", "image/cgm"},
            {".class", "application/octet-stream"},
            {".cpio", "application/x-cpio"},
            {".cpt", "application/mac-compactpro"},
            {".csh", "application/x-csh"},
            {".css", "text/css"},
            {".dcr", "application/x-director"},
            {".dif", "video/x-dv"},
            {".dir", "application/x-director"},
            {".djv", "image/vnd.djvu"},
            {".djvu", "image/vnd.djvu"},
            {".dll", "application/octet-stream"},
            {".dmg", "application/octet-stream"},
            {".dms", "application/octet-stream"},
            {".doc", "application/msword"},
            {".dtd", "application/xml-dtd"},
            {".dv", "video/x-dv"},
            {".dvi", "application/x-dvi"},
            {".dxr", "application/x-director"},
            {".eps", "application/postscript"},
            {".etx", "text/x-setext"},
            {".exe", "application/octet-stream"},
            {".ez", "application/andrew-inset"},
            {".gif", "image/gif"},
            {".gram", "application/srgs"},
            {".grxml", "application/srgs+xml"},
            {".gtar", "application/x-gtar"},
            {".hdf", "application/x-hdf"},
            {".hqx", "application/mac-binhex40"},
            {".htm", "text/html"},
            {".html", "text/html"},
            {".ice", "x-conference/x-cooltalk"},
            {".ico", "image/x-icon"},
            {".ics", "text/calendar"},
            {".ief", "image/ief"},
            {".ifb", "text/calendar"},
            {".iges", "model/iges"},
            {".igs", "model/iges"},
            {".jnlp", "application/x-java-jnlp-file"},
            {".jp2", "image/jp2"},
            {".jpe", "image/jpeg"},
            {".jpeg", "image/jpeg"},
            {".jpg", "image/jpeg"},
            {".js", "application/x-javascript"},
            {".kar", "audio/midi"},
            {".latex", "application/x-latex"},
            {".lha", "application/octet-stream"},
            {".lzh", "application/octet-stream"},
            {".m3u", "audio/x-mpegurl"},
            {".m4a", "audio/mp4a-latm"},
            {".m4b", "audio/mp4a-latm"},
            {".m4p", "audio/mp4a-latm"},
            {".m4u", "video/vnd.mpegurl"},
            {".m4v", "video/x-m4v"},
            {".mac", "image/x-macpaint"},
            {".man", "application/x-troff-man"},
            {".mathml", "application/mathml+xml"},
            {".me", "application/x-troff-me"},
            {".mesh", "model/mesh"},
            {".mid", "audio/midi"},
            {".midi", "audio/midi"},
            {".mif", "application/vnd.mif"},
            {".mov", "video/quicktime"},
            {".movie", "video/x-sgi-movie"},
            {".mp2", "audio/mpeg"},
            {".mp3", "audio/mpeg"},
            {".mp4", "video/mp4"},
            {".mpe", "video/mpeg"},
            {".mpeg", "video/mpeg"},
            {".mpg", "video/mpeg"},
            {".mpga", "audio/mpeg"},
            {".ms", "application/x-troff-ms"},
            {".msh", "model/mesh"},
            {".mxu", "video/vnd.mpegurl"},
            {".nc", "application/x-netcdf"},
            {".oda", "application/oda"},
            {".ogg", "application/ogg"},
            {".pbm", "image/x-portable-bitmap"},
            {".pct", "image/pict"},
            {".pdb", "chemical/x-pdb"},
            {".pdf", "application/pdf"},
            {".pgm", "image/x-portable-graymap"},
            {".pgn", "application/x-chess-pgn"},
            {".pic", "image/pict"},
            {".pict", "image/pict"},
            {".png", "image/png"},
            {".pnm", "image/x-portable-anymap"},
            {".pnt", "image/x-macpaint"},
            {".pntg", "image/x-macpaint"},
            {".ppm", "image/x-portable-pixmap"},
            {".ppt", "application/vnd.ms-powerpoint"},
            {".ps", "application/postscript"},
            {".qt", "video/quicktime"},
            {".qti", "image/x-quicktime"},
            {".qtif", "image/x-quicktime"},
            {".ra", "audio/x-pn-realaudio"},
            {".ram", "audio/x-pn-realaudio"},
            {".ras", "image/x-cmu-raster"},
            {".rdf", "application/rdf+xml"},
            {".rgb", "image/x-rgb"},
            {".rm", "application/vnd.rn-realmedia"},
            {".roff", "application/x-troff"},
            {".rtf", "text/rtf"},
            {".rtx", "text/richtext"},
            {".sgm", "text/sgml"},
            {".sgml", "text/sgml"},
            {".sh", "application/x-sh"},
            {".shar", "application/x-shar"},
            {".silo", "model/mesh"},
            {".sit", "application/x-stuffit"},
            {".skd", "application/x-koan"},
            {".skm", "application/x-koan"},
            {".skp", "application/x-koan"},
            {".skt", "application/x-koan"},
            {".smi", "application/smil"},
            {".smil", "application/smil"},
            {".snd", "audio/basic"},
            {".so", "application/octet-stream"},
            {".spl", "application/x-futuresplash"},
            {".src", "application/x-wais-source"},
            {".sv4cpio", "application/x-sv4cpio"},
            {".sv4crc", "application/x-sv4crc"},
            {".svg", "image/svg+xml"},
            {".swf", "application/x-shockwave-flash"},
            {".t", "application/x-troff"},
            {".tar", "application/x-tar"},
            {".tcl", "application/x-tcl"},
            {".tex", "application/x-tex"},
            {".texi", "application/x-texinfo"},
            {".texinfo", "application/x-texinfo"},
            {".tif", "image/tiff"},
            {".tiff", "image/tiff"},
            {".tr", "application/x-troff"},
            {".tsv", "text/tab-separated-values"},
            {".txt", "text/plain"},
            {".ustar", "application/x-ustar"},
            {".vcd", "application/x-cdlink"},
            {".vrml", "model/vrml"},
            {".vxml", "application/voicexml+xml"},
            {".wav", "audio/x-wav"},
            {".wbmp", "image/vnd.wap.wbmp"},
            {".wbmxl", "application/vnd.wap.wbxml"},
            {".wml", "text/vnd.wap.wml"},
            {".wmlc", "application/vnd.wap.wmlc"},
            {".wmls", "text/vnd.wap.wmlscript"},
            {".wmlsc", "application/vnd.wap.wmlscriptc"},
            {".wrl", "model/vrml"},
            {".xbm", "image/x-xbitmap"},
            {".xht", "application/xhtml+xml"},
            {".xhtml", "application/xhtml+xml"},
            {".xls", "application/vnd.ms-excel"},
            {".xml", "application/xml"},
            {".xpm", "image/x-xpixmap"},
            {".xsl", "application/xml"},
            {".xslt", "application/xslt+xml"},
            {".xul", "application/vnd.mozilla.xul+xml"},
            {".xwd", "image/x-xwindowdump"},
            {".xyz", "chemical/x-xyz"},
            {".zip", "application/zip"}
        };
    
        /// <summary>
        /// Returns the Dictionary entry that matches the Extension.
        /// </summary>
        /// <param name="Extension"></param>
        /// <returns></returns>
        public static KeyValuePair<string, string> FindByExtension(string Extension)
        {
            return MimeTypeDictionary.SingleOrDefault(oo => oo.Key.ToLowerInvariant() == Extension.ToLowerInvariant());
        }
    
        /// <summary>
        /// Returns the MimeType that matches the Extension. If no match is found an error is thrown.
        /// </summary>
        /// <param name="Extension"></param>
        /// <returns></returns>
        public static string GetMimeType(string Extension)
        {
            var rslt = FindByExtension(Extension);
            if (!string.IsNullOrWhiteSpace(rslt.Value))
                return rslt.Value;
            else
                throw new ApplicationException("Unknown Extension.");
        }
    
        /// <summary>
        /// Returns the MimeType that matches the Extension. If no match is found the default value is returned.
        /// </summary>
        /// <param name="Extension"></param>
        /// <param name="Default"></param>
        /// <returns></returns>
        public static string GetMimeTypeOrDefault(string Extension, string Default)
        {
            var rslt = FindByExtension(Extension);
            if (!string.IsNullOrWhiteSpace(rslt.Value))
                return rslt.Value;
            else
                return Default;
        }
    
    
        /// <summary>
        /// Returns the Dictionary entry that matches the MimeType.
        /// </summary>
        /// <param name="MimeType"></param>
        /// <returns></returns>
        private static KeyValuePair<string, string> FindByMimeType(string MimeType)
        {
            return MimeTypeDictionary.SingleOrDefault(oo => oo.Value.ToLowerInvariant() == MimeType.ToLowerInvariant());
        }
    
        /// <summary>
        /// Returns the Extension that matches the MimeType. If no match is found an error is thrown.
        /// </summary>
        /// <param name="MimeType"></param>
        /// <returns></returns>
        public static string GetExtension(string MimeType)
        {
            var rslt = FindByMimeType(MimeType);
            if (!string.IsNullOrWhiteSpace(rslt.Key))
                return rslt.Key;
            else
                throw new ApplicationException("Unknown Mime Type.");
        }
    
        /// <summary>
        /// Returns the Extension that matches the MimeType. If no match is found the default value is returned.
        /// </summary>
        /// <param name="MimeType"></param>
        /// <param name="Default"></param>
        /// <returns></returns>
        public static string GetExtensionOrDefault(string MimeType, string Default)
        {
            var rslt = FindByMimeType(MimeType);
            if (!string.IsNullOrWhiteSpace(rslt.Key))
                return rslt.Key;
            else
                return Default;
        }
        
    }
    
    

    Friday, March 18, 2011

    Ruby Cheat Sheet for .NET Developers

    ....or anybody who sucks at OSX/Linux.

    I've been learning Ruby on Rails over the last couple of months and when you've been programming on Windows with Visual Studio for as many years as I have it's a major learning curve to switch to programming Ruby on OSX or Linux. So I've created a cheat sheet to help me with all the little details I routinely have to look up.

    Ruby on Rails Command Line

    Preview Site:
    rails server
    Preview Site as Production:
    rails s -e production
    Test DB and be able to rollback changes:
    rails console --sandbox
    Reset DB:
    rake db:reset
    Modify DB for real:
    rails console
    View ActiveRecord Raw SQL:
    tail -f log/development.log
    Migrate Development DB
    rake db:migrate (DEV)
    Migrate Test DB
    rake db:test:prepare
    Migrate Production DB
    rake db:migrate RAILS_ENV=production
    Create a New DB Migration:
    rails generate migration [MIGRATION NAME] 
    rails generate migration add_email_uniqueness_index
    View Routes:
    rake routes
    Start Autotest:
    autotest


    Output debug info in Model:
    logger.debug @user.attributes.inspect
    Add Debug info to Layout:
    <%= debug(params) if Rails.env.development? %>
    Routes:
    NAMED ROUTE            PATH
    users_path             /users
    user_path(@user)       /users/1
    new_user_path          /users/new
    edit_user_path(@user)  /users/1/edit
    users_url              http://localhost:3000/users
    user_url(@user)        http://localhost:3000/users/1
    new_user_url           http://localhost:3000/users/new
    edit_user_url(@user)   http://localhost:3000/users/1/edit

    RESTFUL Routes:
    GET      /photos           index     display a list of all photos
    GET      /photos/new       new       return an HTML form for creating a new photo
    POST     /photos           create    create a new photo
    GET      /photos/:id       show      display a specific photo
    GET      /photos/:id/edit  edit      return an HTML form for editing a photo
    PUT      /photos/:id       update    update a specific photo
    DELETE   /photos/:id       destroy   delete a specific photo


    Assign if variable is undefined:
    ||=  
    @current_user ||= user_from_remember_token
    Variable Scope:
    $            A global variable
    @            An instance variable
    [a-z] or _   A local variable
    [A-Z]        A constant
    @@           A class variable
    http://www.techotopia.com/index.php/Ruby_Variable_Scope 


    Upgrade to the latest version of rvm
    rvm update --head
    Install a version of Ruby
    rvm install 1.9.2
    Working with gemsets
    rvm info                        # show the current environment
    rvm 1.8.7                       # use the ruby to manage gemsets for
    rvm gemset create project_name  # create a gemset
    rvm gemset use project_name     # use a gemset in this ruby
    rvm gemset list                 # list gemsets in this ruby
    rvm gemset delete project_name  # delete a gemset
    rvm 1.9.1@other_project_name    # use another ruby and gemset
    Default for Project:
    echo "rvm 1.9.1@MyProject" > ~/projects/MyProject/.rvmrc

    Add gem to Gemfile
    Run:
    bundle install
    or 
    bundle update
    or (for self contained)
    bundle pack


    Extract tar.gz:
    tar -zxvf yourfile.tar.gz
    Find a file:
    find . -name "controller.rb" 
    http://helpdesk.ua.edu/unix/tipsheet/tipv1n10.html
    Delete Folder:
    rm -rf
    Add JPG File Extension to Multiple files:
    for f in *; do mv "$f" "$f.jpg"; done

    Remove file from repo:
    git rm --cached


    GitHub

    Checkout:
    git clone git://github.com/crdeutsch/MVC3-Boilerplate.git
    Publish:
    git push origin master


    Enable Git Flow
    git flow init
    Start a Feature
    git flow feature start myfeature
    Finish a Feature
    git flow feature finish myfeature


    Create App:
    heroku create
    Publish:
    git push heroku master
    Migrate DB:
    heroku rake db:migrate
    View Logs:
    heroku console
    File.open('log/production.log', 'r').each_line { |line| puts line }


    Compile CSS:
    compass compile
    Watch project for changes and compile whenever it does:
    compass watch


    New site:
    staticmatic setup my_site
    Preview:
    staticmatic preview my_site
    Build:
    staticmatic build my_site

    OSX

    Screen Capture:

    Full Screen:
    Hold down Apple key ⌘ + Shift + 3 and release all
    Portion of your screen:
    Hold down Apple key ⌘ + Shift + 4 and release all key
    Application window:
    Hold down Apple key ⌘ + Shift + 4 and release all key
    Now, You will see the mouse cursor will change to +
    Press the space bar once


    This list is far from comprehensive, but it's pretty much everything I've had to google at least once to figure out and I plan to keep adding to it as I progress.

    If you have some nuggets to share add them to the comments.

    If lots of people want to contribute we should probably move this to a different format (Wiki, Markdown?) but until then I'll keep maintaining it here.

    Saturday, March 12, 2011

    Make reCAPTCHA Smaller

    I don't recommend this solution BUT if it's your only option he's how to make reCAPTCHA smaller.

    First you'll need to use a custom theme. Add the following before the form element:

    <script type="text/javascript">
    var RecaptchaOptions = {
        theme : 'custom',
        custom_theme_widget: 'recaptcha_widget'
    };
    </script>
    

    Next add the HTML for the custom widget where you want it to show up inside the form.

    <div id="recaptcha_widget" style="display:none">
    
       <div id="recaptcha_image"></div>
       <div class="recaptcha_only_if_incorrect_sol" style="color:red">Incorrect please try again</div>
    
       <span class="recaptcha_only_if_image">Enter the words above:</span>
       <span class="recaptcha_only_if_audio">Enter the numbers you hear:</span>
    
       <input type="text" id="recaptcha_response_field" name="recaptcha_response_field" />
    
       <div><a href="javascript:Recaptcha.reload()">Get another CAPTCHA</a></div>
       <div class="recaptcha_only_if_image"><a href="javascript:Recaptcha.switch_type('audio')">Get an audio CAPTCHA</a></div>
       <div class="recaptcha_only_if_audio"><a href="javascript:Recaptcha.switch_type('image')">Get an image CAPTCHA</a></div>
    
       <div><a href="javascript:Recaptcha.showhelp()">Help</a></div>
    
     </div>
    
     <script type="text/javascript"
        src="http://www.google.com/recaptcha/api/challenge?k=your_public_key">
     </script>
     <noscript>
       <iframe src="http://www.google.com/recaptcha/api/noscript?k=your_public_key"
            height="300" width="500" frameborder="0"></iframe><br>
       <textarea name="recaptcha_challenge_field" rows="3" cols="40">
       </textarea>
       <input type="hidden" name="recaptcha_response_field"
            value="manual_challenge">
     </noscript>
    

    This is standard code from the reCAPTCHA documentation up to this point. The trick I used to make the img and div tags smaller is to set the width in CSS with the !important flag. Add this CSS to your page.

        #recaptcha_image,
        #recaptcha_image img 
        {
            width: 200px !important;
            cursor: pointer;
        }
        #recaptcha_image img:hover
        {
            position: absolute;
            width: 300px !important;
        }
        .recaptcha_only_if_image,
        .recaptcha_only_if_audio
        {
            display: block;
        }
    

    This will make the reCAPTCHA fit in a space a little over 200px wide. I've added an img:hover style to make the image full size when the mouse rolls over it. The :hover style doesn't work on older browsers though so the next step would be to use jQuery to make the image bigger on hover or click. I'll leave that up to you to figure out. ;)

    Sunday, February 20, 2011

    Preserve Telerik MVC Grid Checkboxes When Paging

    This article explains how to preserve the state of input checkboxes in a Telerik ASP.NET MVC Grid control when paging, sorting, and filtering. This solution only works for client side Data Binding. If you're using Server Side Data Binding you'll need to come up with a different solution.

    I'm a big fan of the Grid control. It's a real time saver and fills in a big gap that ASP.NET MVC is missing over WebForms!

    The first step is to maintain a list of checkbox states. Below is the client side javascript you'll need to add to the page that has your MVC Grid. Note, jQuery is required for this to work.

    var selectedIds = [];
    
    $(document).ready(function () {
        //wire up checkboxes.
        $('#YOUR_GRID_ID').on('change', ':checkbox', function (e) {
            var $check = $(this);
            //console.log($check);
            if ($check.is(':checked')) {
                //add id to selectedIds.
                selectedIds.push($check.val());
            }
            else {
                //remove id from selectedIds.
                selectedIds = $.grep(selectedIds, function (item, index) {
                    return item != $check.val();
                });
            }
        });
    });
    

    NOTE: You'll want to change YOUR_GRID_ID to match what you set your Grid's Name to.

    The code above uses the global variable selectedIds to store the values of each selected checkbox in the grid. I use the jQuery live method to attach the change event to every checkbox in the grid. The nice thing about live is that it will catch new checkboxes that are loaded when paging, filtering, or sorting.

    The next step is to restore the checkbox states after each ajax request that changes the grid.

    function onDataBound(e) {
        //restore selected checkboxes.
        $('#YOUR_GRID_ID :checkbox').each(function () {
            //set checked based on if current checkbox's value is in selectedIds.
            $(this).attr('checked', jQuery.inArray($(this).val(), selectedIds) > -1);
        });
    }
    

    NOTE: You'll want to change YOUR_GRID_ID to match what you set your Grid's Name to.

    I use the Grid's OnDataBound client side event, which fires after the Grid is finished loading new data, to parse threw all the new checkboxes and set their checked state based on if their value is in our selectedIds global variable.

    Add the .ClientEvents line below to wire up the onDataBound event:

    @(Html.Telerik().Grid<Web.Models.YOURMODEL>()
        .Name("VideosGrid")
        .ClientEvents(events => events.OnDataBound("onDataBound"))
    )
    

    If this helped you or you have a suggestion to improve it, let me know via the comments!

    Wednesday, February 16, 2011

    Plupload and ASP.NET MVC3

    This post will explain howto integrate Plupload into an ASP.NET MVC3 project. It will probably work with lesser version of MVC without too many changes.

    Integrating it into my project went smoother then any upload plugin I've ever used before. My previous favorite was NeatUpload but I've been having issues with it with large files, and since Plupload supports chunked uploads I figured I'd give it a try.

    Here's what I did to get the Custom Upload example working in MVC.

    First add the following to one of your Controllers or create a new Controller to hold this Action. (most of the credit goes to these two Stackoverflow questions: Question 1, Question 2)

    /// <summary>
    /// Handles chuncked file uploads like the ones from plupload.
    /// </summary>
    /// <param name="chunk"></param>
    /// <param name="name"></param>
    /// <returns></returns>
    [HttpPost]
    public ActionResult Upload(int? chunk, string name)
    {
        var fileUpload = Request.Files[0];
        var uploadPath = Server.MapPath("~/App_Data/Uploads");
        chunk = chunk ?? 0;
    
        //UPDATE 2/17/2011: Removed this since it doesn't work. I recommend setting the unique_names param client side if you want unique names.
        ////find a free filename if this is the first chunk
        //if (!chunk.HasValue || chunk < 1)
        //{
        //    int xx = 1;
        //    while (System.IO.File.Exists(uploadedFilePath))
        //    {
        //        uploadedFilePath = Path.Combine(uploadPath, Path.GetFileNameWithoutExtension(name) + "_" + xx + Path.GetExtension(name));
        //        xx++;
        //    }
        //}
        
        //TODO: cleanup old files
    
        //write chunk to disk.
        string uploadedFilePath = Path.Combine(uploadPath, name);
        using (var fs = new FileStream(uploadedFilePath, chunk == 0 ? FileMode.Create : FileMode.Append))
        {
            var buffer = new byte[fileUpload.InputStream.Length];
            fileUpload.InputStream.Read(buffer, 0, buffer.Length);
            fs.Write(buffer, 0, buffer.Length);
        }
    
        return Content("Success", "text/plain");
    }
    

    NOTE: If you want to secure this Action and you're using Forms Authentication add the [Authorize] attribute above [HttpPost]

    NOTE 2: I'm uploading files to ~/App_Data/Uploads. Create this folder or change the code above.

    Now open or create a View file that you want to add the upload to and add the following HTML:

    <div id="container">
        <div id="filelist">No runtime found.</div>
        <br />
        <a id="pickfiles" href="#">[Select files]</a>
        <a id="uploadfiles" href="#">[Upload files]</a>
    </div>
    

    Add the following Javascript to your View assuming you've placed everything from the js folder in the Plupload zip into the ~/Scripts/plupload/ folder of your project:

    <!--Load 3rd party plupload scripts-->
    <script src="@Url.Content("~/Scripts/plupload/gears_init.js")" type="text/javascript"></script>
    <script type="text/javascript" src="http://bp.yahooapis.com/2.4.21/browserplus-min.js"></script>
    <!--Load plupload and all its runtime scripts-->
    <script src="@Url.Content("~/Scripts/plupload/plupload.full.min.js")" type="text/javascript"></script>
    
    <script type="text/javascript">
    
        $(document).ready(function () {
            var uploader = new plupload.Uploader({
                runtimes: 'gears,html5,flash,silverlight,browserplus',
                browse_button: 'pickfiles',
                container: 'container',
                max_file_size: '2048mb',
                url: '@Url.Action("upload", "home")',
                flash_swf_url: '@Html.ScriptPath("plupload/plupload.flash.swf")',
                silverlight_xap_url: '@Html.ScriptPath("plupload/plupload.silverlight.xap")',
                filters : [
                    {title : "Image files", extensions : "jpg,gif,png"},
                    {title : "Zip files", extensions : "zip"}
                ]
            });
    
            uploader.bind('Init', function (up, params) {
                $('#filelist').html("<div>Current runtime: " + params.runtime + "</div>");
            });
    
            $('#uploadfiles').click(function (e) {
                uploader.start();
                e.preventDefault();
            });
    
            uploader.init();
    
            uploader.bind('FilesAdded', function (up, files) {
                $.each(files, function (i, file) {
                    $('#filelist').append('<div id="' + file.id + '">' + file.name + ' (' + plupload.formatSize(file.size) + ') <b></b>' + '</div>');
                });
    
                up.refresh(); // Reposition Flash/Silverlight
            });
    
            uploader.bind('UploadProgress', function (up, file) {
                $('#' + file.id + " b").html(file.percent + "%");
            });
    
            uploader.bind('Error', function (up, err) {
                $('#filelist').append("<div>Error: " + err.code + ", Message: " + err.message + (err.file ? ", File: " + err.file.name : "") + "</div>");
    
                up.refresh(); // Reposition Flash/Silverlight
            });
    
            uploader.bind('FileUploaded', function (up, file) {
                $('#' + file.id + " b").html("100%");
            });
        });
    
    </script>
    

    NOTE: I'm using Razor as my View Engine. If you are not do a search for @* in the HTML above and replace with <%= %> type syntax.

    NOTE 2: Update the url param in the javascript to point to where your Upload Action is. Mine is in the Home Controller

    You will also need to have jQuery included. I left that out since most people already do, but if you don't you can use this:

    <script type="text/javascript" src="http://www.google.com/jsapi"></script>
    <script type="text/javascript">
        google.load("jquery", "1.3");
    </script>
    

    The only other thing you may want to immediately do is adjust the filters option in the javascript to allow/disallow file types as needed.

    That's it! Hope this was as easy for you as it was for me!

    Tuesday, February 8, 2011

    Fix No Audio in Live Meeting

    Tried to watch mvcConf today and Live Meeting would not give me any audio.

    Make sure you have the latest version of Live Meeting:
    http://www.mvcconf.com/?l=livemeeting

    The fix that worked for me was the change the default Playback Device:
    1. Right click the Audio/Speaker Icon in the Windows Task Bar and select Playback devices
    2. Right click a different playback device and select Set as Default Device

    If that doesn't work go back into Live Meeting and play around with the Audio options:
    1. In Live Meeting select the Voice & Video menu
    2. Click Options -> Setup Audio and Video
    3. Try changing the Speaker you want to use.

    Thursday, January 20, 2011

    Android Browser Issues

    Ran into a really frustrating issue with Android's Webkit browser today. I'm using jQuery Mobile in a project but have tweaked it quite a bit. The site works awesome on an iPhone. Android (2.2.1) not so much.

    The symptoms:
    1) Regular html select inputs where not opening up with the list of options.
    2) Certain links and buttons had difficulty registering clicks.
    3) One of my text boxes would get scrolled to the top of the page as soon as you started entering text.

    The solution?
    Make sure -webkit-backface-visibility is not set to hidden on any parent elements. The jQuery Mobile CSS has it set to hidden for the ui-page class. Overriding it for android by setting it to visible fixed all 3 of the above issues.

    This took about 6 hours to solve.

    Kill me now.

    Sunday, January 16, 2011

    MVC3 Boilerplate

    Every time I create a new ASP.NET MVC3 project there are certain libraries and code I re-use regularly. One of them being HTML5 Boilerplate which I love. I did some googling for "MVC3 Boilerplate" and didn't find anything, so I decided to start my own and placed it here on GitHub.


    With every release MVC3 included more awesome out of the box (finally has jQuery AND jQuery UI!), but I'd like to see the option to go further. One of the things I love about Ruby on Rails is it includes more of what you need to get you straight to coding like a de facto ORM (ActiveRecord). I also don't like how complicated AspNetSqlMembershipProvider is and the fact it doesn't store data in cleanly named "Users" table so I decided to add my own simple User class that can be modified and extended.


    I'd love to see someone with more experience then me clean up, take over, re-do or otherwise improve on this idea. So feel free to fork my project or make suggestions. I'm not always the best and keeping projects up to date but I'll see what I can do.


    Some of the features I've included are:
    • HTML5 Boilerplate
    • Elmah (error logging)
    • JSON Parser (comes in handy when making JSON based ajax calls)
    • Modernizr (part of HTML5 Boilerplate, but awesome enough to warrant its own mention)
    • AntiXSS Library (Most of the places this is used was based on the Tekpub MVC2 Starter Site, I'm probably doing it wrong and/or not using it enough)
    • Ninject (dependency injection)
    • SquishIt (used to compress and minimize javascript and CSS)
    • Sql Server CE (included so you don't need full MS SQL or SQL Express)
    • EF Code First (used as the ORM)
    • Bits from Tekpub MVC 2 Starter Site
    • Basic User Signup using simple POCO User object

    I really don't know how much I'll keep this project up to date but even if it helps one person that's enough for me. ;)

      Tuesday, January 4, 2011

      Roku Support 2011 Bug

      This is the fun chat I had with Roku support today. To be fair this was an honest mistake and totally excusable, but what I'm not impressed with is the reason I had to contact them in the first place. I bought 2 Rokus as gifts last year. My sister's Roku had an issue and she contacted their support. They determined it needed to be replaced but they needed proof of purchase which means I had to contact them. Grrr. When I called they only offered a fax number to fax the email receipt from Amazon. I was finally able to get an email from Tom, but after forwarding the receipt they want me to call back later to confirm they got it and to proceed with the RMA. So I have to contact them 3 times to RMA a gift. Kill Me.