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.