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 Thomas Ardal has created a nice little webconfig transformation tester, 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 accidentally 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!