Plupload Custom File Upload UI

Plupload is a tool that allows you to upload files using HTML5 Gears, Silverlight, Flash, BrowserPlus, normal forms, and provides features such as upload progress, image resizing and chunked uploads.

So I’m gonna discuss the process I went through to implement a custom Plupload UI.

Here is a video off it in action:

Now I’ll go into details on how it was built using the Plupload framework.
I modeled my version of the plupload custom example here.

So first of all I Init an instance of plupload and set the options. I set the drop area, and the container that the file progress will be displayed in.
Note: Only FF and Chrome support drag’n drop in Plupload.

$(document).ready(function(){
var uploader = new plupload.Uploader({
	// General settings
	runtimes: 'html5,flash,gears,browserplus,silverlight,html4',
	url:<%= SaveUrl %>,
	browse_button : "pickfiles",
	button_browse_hover : true,
	drop_element : "dropArea",
	autostart : true,
	max_file_size: '100mb',
	container: "FileContainer",
	chunk_size: '1mb',
	unique_names: true,
	// Flash settings
	flash_swf_url: <%= "'"+  Page.ResolveUrl("~/plugins/plupload/plupload.flash.swf") +"'" %>,
	// Silverlight settings
	silverlight_xap_url: <%= '"'+ Page.ResolveUrl("~/plugins/plupload/plupload.silverlight.xap") +'"' %>
});

Plupload doesn’t automatically hide the drop area when the current browser doesn’t support it so I bind some custom logic to the plupload “Init” event below. Drag and drop support requires that the browser support FileReader and will not work under the flash or silverlight runtimes.

uploader.bind('Init', function(up, params){
try{
    if(!!FileReader && !((params.runtime == "flash") || (params.runtime == "silverlight")))
         $("#dropArea").show();
         $("#fileSelectMsg").hide();
    }
catch(err){}});
uploader.init();

So now we need to be able to display files in our “FileContainer” div the script below fires on the FilesAdded event. It is called when a file is added or dropped in the drop area. When this event is fired I create a div that contains the elements we will need to display upload progress and the name of the file.

uploader.bind('FilesAdded', function(up, files) {
	$.each(files, function(i, file) {
		$('#filelist').append(
			'<div id="' + file.id + '" class="fileItem"><div class="name">' +
			file.name + '</div><a href="#" id="cancel'+file.id+'" class="cancel">cancel</a><div class="fileInfo"><span class="size">' + plupload.formatSize(file.size) + '</span>' +
			'<div class="plupload_progress"><div class="plupload_progress_container"><div class="plupload_progress_bar"></div></div></div>'+
			'<span class="percentComplete"></span></div></div>');

		//Fire Upload Event
		up.refresh(); // Reposition Flash/Silverlight
		uploader.start();

		//Bind cancel click event
		$('#cancel'+file.id).click(function(){
			$fileItem = $('#' + file.id);
			$fileItem.addClass("cancelled");
			uploader.removeFile(file);
			$(this).unbind().remove();
		});

		//Set ico_Ok to Uploading
		$confirmNext.attr("disabled", "disabled");
		$confirmNext.html("Uploading..");
		$confirmNext.unbind("click").click(function(e){e.preventDefault();});
	});
});

Now that we have files uploading we need to update the progress bars for each file div in the “FileContainer”. So yet again Plupload conviently has an event called “UploadProgress” that fires after each chunk has been successfully uploaded.

uploader.bind('UploadProgress', function(up, file) {
	var  $fileWrapper = $('#' + file.id);
	$fileWrapper.find(".plupload_progress").show();
	$fileWrapper.find(".plupload_progress_bar").attr("style", "width:"+ file.percent + "%");
	$fileWrapper.find(".percentComplete").html(file.percent+"%");
	$fileWrapper.find('#cancel'+file.id).remove();
});

So now we have files uploading and being added to the container and updating the upload progress in real time. So now I want to grey out the div once the file has been completed. In order to do this I add a class to that file’s div. The “FileUploaded” event gets fired after each file is successfully uploaded.

uploader.bind('FileUploaded', function(up, file) {
	$fileItem = $('#' + file.id);
	$fileItem.addClass("completed");

	$confirmNext.removeAttr("disabled");
	$confirmNext.html("Ok");
	$confirmNext.unbind().click(function(e){window.location.href = window.location.href;});

	$('#cancel'+file.id).unbind().remove();
});

Last but not least I’ll show you how to display errors that happen server side when a chunk is uploaded. On “ChunkUploaded” event I check the response message if it contains “Error:” in the response string. I kill the current upload and display that error message to the user and also log the error in my application. The following snippet performs just this.

uploader.bind("ChunkUploaded", function(up, file, response){
	//Should return a status 200 if the chunk was uploaded successfully
	if(response.status != null)
	{
		if(response.status != "200" || (response.response.indexOf("Error") != -1))
		{
			if(response.response.indexOf("Error") != -1)
			{
				//Prompt the user with the custom error message
				$("div.error:first").show().html('<p>'+response.response+'</p>');
			}
			else
			{
				//Log this as an error
				//Custom line of code to log error on server would go here
				
				//Notify user of error
				$("div.error:first").show().html('<p>There was an error uploading your file '+ file.name +' Support has been notified.</p>');
			}
			$('#' + file.id).addClass("cancelled");
			uploader.stop();
		}
	}    
});

So thats how I built my custom UI wrapper around the Plupload core. Hopefully that will help get you going in the right direction.

Integrating Bing Search Results Within A Web App Using .NET

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

A few months ago I was faced with the challenge of including a site search in a web system we maintain.

The main goals for the search tool were meaningful results, ease of use, and low cost of implementation.

After evaluating the search offerings from Google, Bing and Yahoo we went with Bing. Microsoft offers high flexibility and no fees for using the Bing Search API. The only thing they request is that you show some type of image or statement crediting Bing with the search results.

The Bing Search API is part of Project Silk Road it offers many options for choosing the source type and output protocol (JSON, SOAP, XML) as well as flexible presentation options (No restrictions on ordering and blending of result). This allows you to integrate custom results with the Bing results without violating the usage terms.

Below are some code snippets to reference:

Retrieves the XML document containing search results from Bing and loads them into an object collection.

public BingSearchResult(string title, string description, string url)
{
     this.Title = title;
     this.Description = description;
     this.Url = url;
}

private static BingSearchResultCollection GetResults(string searchQuery, int pageNum, int resultsPerPage)
{
int offset = ((pageNum - 1) * resultsPerPage);

string url = "http://api.search.live.net/xml.aspx?Appid={0}&sources={1}&query={2}&{1}.count={3}&{1}.offset={4}";
string completeUri = String.Format(url, appID, "web", "site:www.yoursite.org " + searchQuery, resultsPerPage, offset);

HttpWebRequest webRequest = null;
HttpWebResponse webResponse = null;
XmlReader xmlReader = null;
webRequest = (HttpWebRequest)WebRequest.Create(completeUri);
webResponse = (HttpWebResponse)webRequest.GetResponse();
            
//Handles an invalid xml document
try
{
    xmlReader = XmlReader.Create(webResponse.GetResponseStream());
}
catch (System.Security.SecurityException)
{
    return new BingSearchResultCollection();
}

XDocument xmlDoc = XDocument.Load(xmlReader);
            
//Loops through the results and creates BingSearchResult objects
if (GetElementsByName(xmlDoc, "SearchResponse").Any())
{
    XElement searchResponse = GetElementsByName(xmlDoc, "SearchResponse").First();
    if(GetElementsByName(searchResponse, "Web").Any())
    {
        XElement web = GetElementsByName(searchResponse, "Web").First();
        if (GetElementsByName(web, "Total").Any())
        {
                        
            int totalResults = Convert.ToInt32(GetElementsByName(web, "Total").First().Value);
            var list = new BingSearchResultCollection(totalResults, searchQuery);

            if (GetElementsByName(web, "Results").Any())
            {
                XElement results = GetElementsByName(web, "Results").First();
                if (GetElementsByName(web, "Results").Any())
                {
                    if (GetElementsByName(results, "WebResult").Any())
                    {
                        var webResults = GetElementsByName(results, "WebResult");

                        foreach (XElement el in webResults)
                        {
                            string resTitle = "";
                            string resUrl = "";
                            string resDescr = "";
                            if (GetElementsByName(el, "Title").Any())
                                resTitle = GetElementsByName(el, "Title").First().Value;
                            if (GetElementsByName(el, "Url").Any())
                                resUrl = GetElementsByName(el, "Url").First().Value;
                            if (GetElementsByName(el, "Description").Any())
                                resDescr = GetElementsByName(el, "Description").First().Value; ;

                            if (IsValidUrl(resUrl))
                                list.Add(new BingSearchResult(resTitle, resDescr, resUrl));
                        }

                        return list;
                    }
                }
            }
        }
    }
}

return new BingSearchResultCollection();
}

private static IEnumerable<XElement> GetElementsByName(XContainer cont, String name)
{
     return (from el in cont.Elements()
          where el.Name.LocalName == name
          select el);
}

The reason I ended up using a custom LINQ function to traverse the tree was because I couldn’t figure out how to get a collection of WebResult elements.

The XML document returned from Bing is structured like this:

<?xml version="1.0" encoding="utf-8" ?> 
<?pageview_candidate ?> 
<SearchResponse xmlns="http://schemas.microsoft.com/LiveSearch/2008/04/XML/element" Version="2.2">
     <Query>
          <SearchTerms>site:www.yoursite.com SearchTerm</SearchTerms> 
     </Query>
     <web:Web xmlns:web="http://schemas.microsoft.com/LiveSearch/2008/04/XML/web">
     <web:Total>1350000</web:Total> 
     <web:Offset>0</web:Offset> 
     <web:Results>
          <web:WebResult>
               <web:Title>Result Title</web:Title> 
               <web:Description>Description of Result</web:Description> 
               <web:Url>http://www.resulturl.com</web:Url> 
               <web:CacheUrl>http://cc.bingj.com/cache.aspx?somecashobjecthere</web:CacheUrl> 
               <web:DisplayUrl>www.resulturl.com</web:DisplayUrl> 
               <web:DateTime>2010-07-02T20:27:32Z</web:DateTime> 
         </web:WebResult>
     </web:Results>
     </web:Web>
</SearchResponse>

Hopefully this gives you an idea of what you need to hit the floor running when implementing the Bing Search API in your project(s).

Here are some helpful resources:
http://www.bing.com/developers/
http://www.bing.com/developers/s/API%20Basics.pdf

If you need a powered by Bing image you can use the one I created here.

Internationalization Data Storage in .NET (Part 1)

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

.NET has many built in structures for setting up your web application to support multiple languages. Most of which reside in the System.Globalization namespace. Within the System.Globalization namespace you get access to the CultureInfo class. This class defines how datatypes such as Dates, Currency and other culture related objects are displayed.

For example if the CultureInfo object is set to “mx-es” (spanish) the date would be displayed dd/mm/yyyy instead of mm/dd/yyyy. You can manage all the output settings by setting their respective properties in the CultureInfo class.

Now that we have a very basic idea of what the built in .NET tools provide us in terms of displaying internationalized data lets take a look at the solutions we have for storing globalized data.

So lets say we have a database table called Product with a field called “Description” we want to internationalize:

ProductID Name Description Date Added
1 Vista If you own or run a small business, you’ll want Windows Vista Business. 6/22/2009 3:57:07 PM

To store the multiple langages we could add a columns for each language to the table structure and subsequently update all related dependencies. Storing values in this manner is something we want to avoid here at LANIT.

Another option would be to build a look up table that stores translated values and replaces our description with a reference. This method would allow for efficiently searching and indexing but would increase the complexity of our database structure.

In our situation we decided to store every language into an XML document. The benefits of doing it this way is it allows us to add infinitely many translated versions of the product name without affecting are database structure. The draw back of using this method is the inability to efficiently search through the database on the internationalized column(s).

Let’s say we wanted to store English/French/Spanish/German translations of our product description we could do so by generating an XML file that looks similar to this.

<?xml version="1.0" encoding="utf-8" ?>
<localizedString>
<value language="en-US"> If you own or run a small business, you'll want Windows Vista Business.</value>
<value language="es-MX">Si usted es dueño o ejecutar un pequeño negocio, usted desea que Windows Vista Business.</value>
<value language="fr-FR">Si vous êtes propriétaire ou de lancer une petite entreprise, vous souhaitez que Windows Vista Business.  </value>
<value language="de-DE">Wenn Sie selbst oder ein kleines Unternehmen, Sie wollen Windows Vista Business.</value>
</localizedString>

Each value element represents a language and has an attribute called language which is set to a culture name. A list of culture names can be found here MSDN Culture Names

Updated Table Data:

ProductID Name Description Date Added
1 Vista <?xml version=”1.0″ encoding=”utf-8″ ?> 6/22/2009 3:57:07 PM

This provides us with a data storing model that will fit very easily into the current database structure and also provide us the ability to scale to as many languages as we see fit in the future.

That is it for part 1 . In part 2 I will discuss building a class/datatype that will get,set and build the above XML document for storing our translated values.