Showing posts with label Tips. Show all posts
Showing posts with label Tips. Show all posts

Tuesday, March 18, 2008

Stable Sort

.Net does not use a stable sort for its List<t> Sort method. Watch out!

A stable sort is a sort that preserves the original order for items that are considered equal.

For example, take the following list of objects:

Name, Group
John, Group 3
Sally, Group 2
Bill, Group 2
Joe, Group 3
Alf, Group 1

Now, sort these objects on the 'Group':

Alf, Group 1
Sally, Group 2
Bill, Group 2
Joe, Group 3
John, Group 3

You'll notice that the list is correctly sorted by group and the names are ignored. That is as it should be. You will also notice that Joe appears before John where in the previous list he did not. That is going to cause problems and here is why. Resort the sorted list on Group again:

Alf, Group 1
Bill, Group 2
Sally, Group 2
John, Group 3
Joe, Group 3

You'll notice that the list is correctly sorted by group but that the names have shuffled around. If you resort a third time you'll get back to the 1st sorted list. Logically you'd expect an already sorted list not to be changed by a sort algorithm. That kind of sort is called a stable sort and .Net doesn't use one. It's not a bug and it's not wrong but it may not be what you expected.

The solution is to use a stable sort algorithm by rolling your own or by sorting on unique keys. For our example we'd sort on Group then Name in order to get a determinate sort order from the unstable sort algorithm.

Tuesday, March 11, 2008

Firefox, Transparent Proxy and Authentication

Firefox is my browser of choice. But it does not always integrate smoothly into the office environment.

In our environment Internet access is made through a proxy server. We are required to authenticate to the server using our Active Directory credentials before access is granted.

Recently we switched over to a transparent proxy. This has been a good thing as it simplifies configuration, doesn't require changing proxy settings when I carry my laptop home, and pretty much just works automatically.

A few applications don't like authenticating to a proxy server but Firefox does very well. I am prompted for my credentials when I first open Firefox and using the Remember Password feature I usually don't have to enter the credentials but just click OK. But some things just don't work. The Search Suggest drop down causes an authentication dialog to appear (and this dialog doesn't remember my credentials) and theme, extension and automatic updates fail.

Solution

Firefox will handle authenticating proxies gracefully when configured to do so. There is no UI for setting this up but the configuration changes are easy to make using about:config. Here is how:

  1. Determine the domain name of your proxy server. You might have to ask your IT department but you can probably glean this information from the authentication dialog.

  2. Open a new tab and enter about:config in the address bar. Type negotiate in the Filter field. The preferences list should be reduced to about 5 items.

  3. Find the network.negotiate-auth.trusted-uris preference. Double click it and enter the proxy server's domain name.

  4. Find the network.negotiate-auth.delegation-uris preference. Double click it and enter the proxy server's domain name here as well.

That should do it. Firefox will automatically use your Windows credentials to authenticate to the proxy server. You should not need to make any changes when connected outside the corporate network.

Source

Wednesday, January 16, 2008

Instead of Enum

Since I tend to ramble I'll start this out with the code.



using System;
using System.Collections.Generic;
using System.Reflection;

public class MyEnum
{
#region Fields
private int id;

// Other values
private string name;
private bool archived_flag;
#endregion

#region Static Pre-defined instances (this is the enum)
public static readonly MyEnum Empty = MyEnum.Factory(-999, string.Empty);
public static readonly MyEnum First = MyEnum.Factory(1, "First Item");
public static readonly MyEnum Second = MyEnum.Factory(2, "Second Item");
public static readonly MyEnum Third = MyEnum.Factory(3, "Third Item");
public static readonly MyEnum Fourth = MyEnum.Factory(4, "Fourth Item");
#endregion

#region Constructors
private MyEnum()
{

}
#endregion

#region Properties
public string ID
{
get
{
return this.id;
}
}
public string Name
{
get
{
return this.name;
}
}

#endregion

private static MyEnum Factory(int id, string name)
{
MyEnum me = new MyEnum();
me.id = id;
me.name = name;

return me;
}

public static MyEnum GetByID(int id)
{
if (lAll == null) { MyEnum.GetAll(); }
foreach (MyEnum me in lAll)
{
if (me.id == id)
{
return me;
}
}
return MyEnum.Empty;
}

public static List lAll = null;

public static List GetAll()
{

if (lAll == null)
{
lAll = new List();

Type myType = typeof(MyEnum);
FieldInfo[] fields = myType.GetFields();

foreach (FieldInfo fi in fields)
{
if (fi.FieldType == myType && fi.IsStatic && fi.IsPublic)
{
MyEnum value = fi.GetValue(null) as MyEnum;
if (value != null)
{
lAll.Add(value);
}
}
}
}

return lAll;

}

#region Operators
public override bool Equals(object obj)
{
if (obj == null) return false;

MyEnum co = obj as MyEnum;
if ((object)co == null) // Cast to avoid calling the override == operator
{
return false;
}

return this.Equals(co);
}

public override int GetHashCode()
{
return base.GetHashCode();
}

public bool Equals(MyEnum me)
{
return (object)me == null ? false : this.ID.Equals(me.ID);
}
public static bool operator ==(MyEnum a, MyEnum b)
{
// If the same instance or both null then return true
if (System.Object.ReferenceEquals(a, b))
{
return true;
}

// If one is null return false
if ((object)a == null (object)b == null) // Cast to avoid calling the override == operator
{
return false;
}

return a.Equals(b);
}
public static bool operator !=(MyEnum a, MyEnum b)
{
return !(a == b); // This calls the override operator above
}
#endregion

public override string ToString()
{
return this.Name;
}

}


I use this pattern as a replacement for almost all my former enums. Using classes like the one above you get a simple type with a set of pre-defined, valid, values (like an enum). It's type-safe. It handles equals(), == and !=. It also allows you to add properties and methods to each value which provides much more programming power.

Wednesday, October 31, 2007

JavaScript Date Object Tip

Quicktip: Nov. 31, 2007 == Dec. 1, 2007

Background
I had to repair a JavaScript based calendar popup tool today. For some reason November was not being displayed correctly. It had the correct number of days but November 1st was on Saturday, not Thursday.

This issue only manifested itself when the current date is the 31st day of the month and only caused display problem for months with less than 31 days.

What is going on?
This issue turned out to be related to an odd behavior of the JavaScript date object so I thought I'd share.

Let's go over the "offending" code:

var today = new Date(); // new Date(2007, 9, 31);
This creates a new date object with Oct. 31, 2007 (which is today)

today.setMonth(10);
This sets the month to November

alert(today);
This shows an alert box with Dec 1, 2007

But wait!! Didn't we set the month to November? Well, we did, but November only has 30 days. October had 31 days. So I guess we set the date to be Nov. 31. You could say that Nov. 31 is the same as Dec. 1. You might not say that but JavaScript does.


Solution(s):
1. Set the day of the month before setting the month.
2. Create a new date object using appropriate parameters: new Date(2007, 10, 1);

Monday, June 18, 2007

Checkboxes in Repeaters (ASP.Net 2.0)

Every 3 or 4 months I build a website that uses a list of check boxes to allow the users to make a selection from a list of items.

Every 3 or 4 months I fight, curse, and swear at ASP.Net and its databound controls (okay, I don't curse or swear but I do fight).

So, I've just finished doing it all again and this time I'm going to write it down so I don't forget how it's "supposed to be done".

Scenario

I have a list of thingies that I want the user to choose from. I list the thingies out in a repeating control (I use the asp:repeater but a asp:datalist behaves the same).

The user checks off the items they want and clicks a button firing off a server side onClick event.

The Problem

You loop through all the check box controls and none of them are checked. A secondary problem is that the asp:checkbox control does not have a "Value" property to store useful information about what the checkbox might be tied to.

"Wrong" Solutions

After searching the Google for help and only finding posts from 2003 I figured that either everyone gave up or this isn't really a problem for anyone but me.

One search hit suggested that I make sure I DataBind my repeater on every post back (in other words, don't put my data binding inside the !Page.IsPostback block of my Page_Load). This didn't work.

Someone suggested creating a hidden control to store a Value (since asp:checkbox doesn't have a value property), but doesn't really seem to address the issue of all the checkbox check states missing when you post back.

I found a suggestion that said making the itemcommand event trigger and then looping through the all items on the repeater would work. I think this method would allow you to get the dataitem associated with the checkbox and might preserve the check states. However, I wasn't able to get something workable using this technique. And it feels a bit wrong to use itemcommand which is intended for a single "row" or item and apply it for the entire repeater.

"Right" Solution

So the solution I found is to use the HtmlInputCheckbox control instead of the asp:Checkbox control. It really is that simple. It has a Value property and it maintains its checked value after the postback.

To retrieve the check states and values simply loop through each item in the repeater and get the html input check box using FindControl. Do this before re-binding the repeater.

Example (ASP.Net 2.0)

HTML (test.aspx)

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="test.aspx.cs" Inherits="test" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<h2>Which are your favorite Beatles?</h2>
<table cellspacing="1">
<tr >
<td>&nbsp;</td>
<td>Name</td>
</tr>
<asp:Repeater ID="repList" runat="server">
<ItemTemplate>
<tr>
<td>
<input type="checkbox" id="cbItem" runat="server" />
</td>
<td>
<asp:Literal ID="litName" runat="server" />
</td>
</tr>
</ItemTemplate>
</asp:Repeater>
</table>
<p><asp:button ID="btnVote" text="Vote" onclick="btnVote_Click" runat="server" /></p>
<p><asp:Label ID="labResult" runat="server" /></p>
</form>
</body>
</html>


Codebehind (test.aspx.cs)

using System;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;

public partial class test : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
this.repList.ItemDataBound += new RepeaterItemEventHandler(repList_ItemDataBound);

// Load the class data
if (!Page.IsPostBack)
{
this.BindRepeater();
}
}

protected void BindRepeater()
{
List<ListItem> items = new List<ListItem>();
items.Add(new ListItem("John", "1"));
items.Add(new ListItem("Paul", "2"));
items.Add(new ListItem("George", "3"));
items.Add(new ListItem("Ringo", "4"));

this.repList.DataSource = items;
this.repList.DataBind();
}

protected void btnVote_Click(object sender, EventArgs e)
{
string vote_results = string.Empty;

foreach (RepeaterItem ri in this.repList.Items)
{
HtmlInputCheckBox hcb = ri.FindControl("cbItem") as HtmlInputCheckBox;
if (hcb != null)
{
if (hcb.Checked)
{
if (hcb != null)
{
int id = Convert.ToInt32(hcb.Value);
vote_results += id + " ";
}
}
}
}

this.labResult.Text = "You voted for " + vote_results;
this.BindRepeater();
}

protected void repList_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
ListItem item = e.Item.DataItem as ListItem;

if (item == null) return;

Literal litName = e.Item.FindControl("litName") as Literal;
HtmlInputCheckBox cbItem = e.Item.FindControl("cbItem") as HtmlInputCheckBox;

litName.Text = item.Text;
cbItem.Value = item.Value;
}
}

Friday, May 04, 2007

HTML <button> tag

I am frequently amazed at the new "old things" I discover as I continue in web development. For example, I'm coming around to using the H tags and the P tag for markup. I've always used spans, divs, and brs.

My most recent "discovery" is the button tag. This little jewel has its problems but it gives me some of the flexibility that an input type="button" just doesn't provide.

See for yourself:



The <button> tag accepts HTML content rather than a simple text value. As far as I know (I don't know much) you can put most anything in there.

It is important to remember that <button> has a type attribute that determines whether it behaves as a submit, reset, or simple button.

Some caveats:

IE does not handle form submission of <button>s correctly.
  • Rather than submitting the value (as specified in the value attribute) it submits the inner HTML of the tag.
  • IE submits all the <button> tags; not just the button that was clicked. This makes it so the server-side script is unable to tell which button was clicked.

Work-arounds:
  • Set the type attribute to button and use JavaScript. If you can't use JavaScript then I'm afraid you may not have much success with the <button> tag (especially if you want to use more than one <button> tag in a form)
  • Button Value: Use the JavaScript onclick event to populate a hidden field with the value from the button. In your server-side scripts you can read out values from the hidden fields and ignore the buttons.
  • Which Button Clicked: Use the JavaScript onclick event to populate a hidden field with a value that identifies which button was clicked. On the server-side use the hidden field to determine action rather than the button.
ASP.Net already uses similar methods for its PostBacks and so these work-arounds aren't necessary. Just remember to set the type attribute to button or you will get two postbacks when a button is clicked. Bind click events in ASP.Net by using the onserverclick event.

Thursday, April 12, 2007

Destroying ASP.Net with an IFRAME

Today, while implementing a "Thank you after download" page using the iFrame method, my page suddenly began throwing ArgumentExeption: Invalid postback or callback argument. I could not, for the life of me, figure out what was causing it.

I wasn't doing anything funny with any server-side controls and the page had been working just fine before I added the download code.

I turns out that I was using some HTML like this:

<iframe src="http://website.com/download" style="display: none;" />


Which is perfectly acceptable XHTML (as far I've bothered to find out). But ASP.Net has a conniption. You have to format it like this:

<iframe src="http://website.com/download" style="display: none;"></iframe>


So there you have it. If you don't use a closing tag on an iFrame in ASP.Net 2.0 it will throw a fit and you'll spend hours wondering what is going on.

How To: Redirect After Download

Don't!

I don't think you can. I googled a bit and gave up. Then I decided to check a page where I've seen it done: Source Forge. Turns out that they cheated!

So, the answer is still "don't", however, we can do what SF does and...fake it!

What I want to happen:
  1. Our user sees a link to a download and clicks it.
  2. The Save As... dialog appears and the user selects the location.
  3. The browser displays a "Thank You" page while the download finishes in the download window.

How to Fake it:
  1. Our user sees a link to a download and clicks it.
  2. The link takes the user to the "Thank You" page. The "Thank You" has an iFrame whose src attribute is the real download link.
  3. The Save As... dialog appears and the user selects the location.
  4. The download finishes while the user views the "Thank You" page.

Discussion:

There could be compatibility issues with an iFrame in mobile or really old browsers. Some alternatives might be:
  • Use a <meta> tag refresh pointing to the real download link and a time of 0 (zero).
  • Try a Javascript redirect.
But the idea is to fake it by displaying the "Thank You" page first and then starting the download.

If all else fails just rethink your download flow. The user clicks the download link. He gets the "Thank You" page which has a very clear, front and center link that says "Begin Your Download by clicking here" or something that says "I'm really sorry you have to click twice (it's not my fault you use a lame browser) so please accept this flattering compliment (to help avoid the total destruction of the world (and I'm sorry your self-esteem is so fragile))."

Friday, March 30, 2007

SWFUpload, Firefox and ASP.Net Sessions

A solution to the Firefox session cookie issue with Flash and SWFUpload was posted to the SWFUpload forums at swfupload.mammon.se

I'll reproduce it here when I've got permission and probably adapt SWFUpload R4 to send the cookie value you specify in the settings.

In the meanwhile you can view the post at the SWFUpload Forum

Thursday, February 15, 2007

Taskbar Shuffle

The Windows Taskbar has always annoyed me. I always thought Windows 3.1 had it right. (Don't get me started on helping Grandma find the documents she saved to the desktop rather than My Documents)

My biggest issues with the Taskbar are:
  • I like to have my "tasks" in a particular order. I have, in fact, closed all my other programs and re-opened them just to return them to the proper order on the Taskbar.
  • I like related tasks to be grouped together. Especially multiple windows from the same application.
  • If I accidentally close a program I don't want it to re-open at the end of the task list. I want it to be where it was. Outlook always goes first, if it's suddenly last it takes me ages to find it.
So, Microsoft finally made some improvements in Windows XP with their Task Grouping settings but it still doesn't really cut it.

You can only imagine how happy I was when I found Taskbar Shuffle. It does one wonderful thing. It lets me click and drag the tasks allowing me to re-order them any way I want! So what if it did a funny inverted colors thing.

The 2.0 version was recently released and things are only better. It looks a lot better for one thing. And it has a new grouping features that group new windows from the same application (I mean you Internet Explorer) together. This isn't the "collapse into a single task" grouping. It's just keeping the tasks next to one another.

So, I highly highly recommend this great little utility!

(no, I'm not advertising or have any monetary interest in Taskbar Shuffle. A great app just deserves high praise!)

Wednesday, February 14, 2007

SWFUpload - The Back End

I've received some more feedback and I've finished my first implementation of SWFUpload R3 in one of my own websites (which is on an Intranet so, unfortunately, I can't show it off).


Issues of note:
  • My code uses document.getElementById to access the Flash movie. I received a report that this doesn't work in Firefox on OS X. document.embeds was recommended as an alternative. The original SWFUpload used a similar method. If you use document.embeds make sure you test in IE on Windows. IE used the "Object" tag rather than the "Embed" tag.
  • Using SWFUpload in FireFox behind a Proxy that requires HTTP Authentication does not work for me. It does prompt for a username/password but will not accept it. In fact Flash locks up Firefox and you have to retype your blog posts from scratch.
  • You can still force the IE Forms issue from the original SWFUpload if you insist on loading the flash inside a form without using the window.onload event. Don't do it, that was the whole point of SWFUpload R3.
  • In my testing I found that Flash in Firefox (2.0) does not send the Firefox cookies to the upload_backend file. It seems to send the IE cookies instead. This seems like a security issue, but maybe it's just happening to me. I should do some more testing. However, in many environments if your cookies aren't sent then you will have lots of problems with user authentication and sessions.

Build the Back-End

Receiving and processing the uploaded file is a bit tricky. At least it was for me since I had to work around the FireFox Flash Cookie issue I experienced (and explained above).

Here are some main points to remember:
  • You may not be able to rely on the cookies that flash sends (for Sessions and authentication)
  • The only way to notify SWFUpload of an error in the upload is to return an appropriate HTTP status code. You will probably want to send a HTTP 500 status and then handle the error SWFUpload sends to the page.
  • You can send values to the upload backend using the query string. E.g. upload_backend : "upload.php?fileid=ABCDEFG0123456789&bob=john".
  • Each upload is sent via a separate request. You don't have to worry about handling multiple uploads. You only have to handle the one upload in the Filedata post value.
SWF Upload sends the following data to the backend:
  • Filedata: This is the name of the file upload and contains all the data about the file.
  • Filename: This contains the name of the file. It is kind of redundant.
  • Upload: In my testing this always contains the string "Submit Query". I have no idea what it is for.

Code Samples:

ASP.Net Sample:
try
{
Request.Files["Filedata"].SaveAs("/path/and/file/name.ext");
}
catch
{
Response.Status = 500;
}



ASP Sample:
if (Request.Upload.Files.Count > 0) then
if (Not Request.Upload.Files("uploadedfile").SaveAs(Server.MapPath(FileName), True)) then
Response.Status = 500
end if
end if



ColdFusion sample:
<cffile action="upload" destination="/path/and/file/name.ext" filefield="form.filedata">



PHP Sample:
if (!move_uploaded_file($_FILES['Filedata']['tmp_name'], "/path/and/file/name.ext")) {
header("HTTP/1.0 500 Internal Server Error");
}


I have not tested these samples. They are built based on sample code from around the web. Feel free to suggest better samples if you've got them.

Tuesday, January 30, 2007

SWFUpload: The Saga Continues...

I've had a little feedback on SWFUpload. And you might want to thumb through the comments for a few hints (there aren't that many).

Macs (they always gotta be different)

It seems on Macs that if your 'upload_backend' does not return any data that the UploadComplete event will never be called. This seems to happen for any browser which indicates that is probably an issue in the Flash for Mac implementation. So, instead of just having a blank page for your 'upload_backend' just throw in some text like, Thank you. Upload Complete. That, reportedly, will solve the problem.

IOError (specifically, Firefox for Windows)

If you get an IOError then you may not be able to upload any more files. The report says the Open File dialog will no longer open. For me the Open File dialog does appear and the UploadStart event gets called but then I never get any UploadProgress callbacks.

Solution: Handle this some way in your error handler. You could alert the user to reload the page. Or use Javascript to remove the offending Flash movie and reload it then let the user know that she needs to try again.

Better Solution: Fix the upload.swf. I realized that the logic in my changes to the Flash file was not very good. When an error occurred upload.swf would just stop processing the queue. Then when you tried to add more files it just tried to upload the erred out file again. So, I've fixed that. There still seems to be occasions when upload.swf just stops responding and you can't get the Open File dialog to come up. You may still have to rely on the "reload the movie" solution discussed above. This still feels like a deeper flash issue that may be part of the immaturity of the file upload features of Flash itself.

Firefox, Cookies and Sessions

I've finally started implemented SWFUpload in one of my own projects. It's been quite the challenge. It turns out that in Firefox for Windows that when SWFUpload uploads the file it sends the Cookies from Internet Explorer. So that means the session set up by ASP.Net (the session_id is stored in a cookie) for Firefox is not re-established when SWFUpload makes the upload. This is a problem I can't fix. I have had to change the way I link the form that my user is filling out and the files she upload since they're submitted separately and I can't rely on any session values.

Another Feature

I also added an upload limit feature. You set the upload limit when instantiating the SWFUpload object and, even if the user queues up more files, the movie won't upload any more files than the limit. I added a new error code (-60) to report back on files rejected because the upload limit has been reached.

Isn't implementation grand!

Wednesday, January 24, 2007

SWFUpload R3 Update

I've finished my final additions to SWFUpload. The "delayed" upload works fine. It is a bit of extra work, especially if you try to carefully take care of errors.

I've updated the Demos and sent a copy back to the original authors. They like the features I've added. Maybe some of them will end up in the official release.

It's been fun. Now it's time to get back to work.


Tuesday, January 23, 2007

SWFUpload Revision 3 Demo

I've completed the major work on my third revision of SWFUpload. I've thoroughly taken the plunge and have made several modifications to the SWFUpload flash file. That, in turn, required several changes to the Javascript file.

I’ve added the following features:
  • SWFObject has been removed in favor of my Flash Feature Detection technique.
  • File Objects now include a unique ID. This is useful for working with the DOM.
  • It is now possible to cancel an upload-in-progress.
  • It is now possible to cancel all the files queued for upload (but not yet uploaded).

I have updated my demo to show off the new features. Again, I don't have a host. Contact me if you want a copy. It's based around asp.net but is easily adapted to any server technology:


Other ideas

I’d like to add a feature that allows you to remove queued files before the upload starts. Now that I have unique file IDs this should be pretty easy.


Trained the wrong way, but still trained

It occurred to me that SWFUpload does not quite fit in with the normal HTTP file upload format that we (and our users) are all used to. Usually we select our files and then probably have to fill in other parts of a form. Once completed we submit the entire form together. SWFUpload is breaking this up. Users might have a tendancy to click the Submit button before their uploads have finished.

I appreciate the benefit of starting uploads immediately. Users can finish the form while the upload is going and it'll all be more efficient. However, that’s not how users have been trained to use websites. They have been trained to complete a form (without looking at its labels). Then you pressing submit to back the message that says you did it all wrong. You trying again (and again) and finally getting it submitted.

With that in mind I think I am going to add one final feature. It will allow you to specify whether or not the queued-up files should be uploaded immediately or should wait until a command to upload is given. Now the user can queue up their files and fill out the form. The Submit button will kick off the SWFUpload file upload (the user still gets the great benefit of watching the file progress and getting feedback about the file size). The uploadQueueComplete event handler is called when the upload finishes and it submits the form. The page can watch for any SWFUpload errors and cancel the submit if necessary.

This is more in keeping with the expected user experience.

I'll keep ya'll posted. Yep, I mean both of you.

Flash Feature Detection

I've continued to work on my SWFUpload revision. I've tested the graceful degradation a bit more and it is slowly improving. I've tested it with Flash 2, 4, 6, and 8 in Internet Explorer 6. Of course, it only works in Flash Player 8 or above but it correctly degrades for early players.

Flash Player Feature Detection
I've also started on a Revision 3 which changes the entire method of Flash Version Detection. I have removed the SWFObject code (which performs the Flash version detection). I'm experimenting with a detection method similar to browser Object Detection. Instead of looking at the flash version number (as reported by the browser) I have the player check for key features, such as, Javascript calls to Flash, Flash calls to Javascript and the whole File Upload bit, which only work in since Flash Player 8.

I do this by having Javascript first add the flash movie to the page. It does not attempt any Flash version checking. If Flash is installed and working in the browser the movie will load. If a Flash Player older than version 8 is installed it will not be able to play the movie properly and the SWFUpload script stops.

However, if Flash Player 8 or above is installed the movie calls a Javascript function which alerts the SWFUpload script that it can continue loading. SWFUpload continues by loading the UI components so the user can begin uploading files.

New SWFUpload features

I didn't think I'd go so far in this project as to begin tweaking the Flash files. But now that I have I am going to begin looking in to adding Cancel Upload and Cancel Queue features.

It's been an interesting excursion so far but I think I've just about spent enough time on it. Hopefully I'll have what I want wrapped up pretty quick.

Thursday, January 18, 2007

SWFUpload Revision 2 Demo

Here I go again. After posting my changes to the SWFUpload script I decided I better throw the script up on a server and make sure it really works. Well, it didn't quite work. I've updated the post a bit so hopefully everything is peachy.

I built an ASP.Net demo that shows off my changes. Unfortunately, I don't have anywhere to host it or to post the files for download. So, if you want a copy of the demo files leave contact info in the comments.

SWFUpload Revisited and Revised

Some discussion about my SWFUpload FORM solution for ASP.Net rekindled my interest in the script. I also re-read my post about SWFUpload and realized that it was a bit incomplete in its explaination. So I've done some more work and I hope this is more helpful.

I also realize that I'm just posting code. You'll have to get the original SWFUpload and make sure all your file names and paths are correct/updated before this example code will work.

The Problem

SWFUpload does not provide a way for having more than one upload object per page. This is probably fine for most cases. However, if you want to upload an image and a word document (with different file size constraints or other settings) you need to have more than one upload object on the page.

So, I've revised the SWFUpload script. I had to restructure the design a bit (SWFUpload was built as a "Static" object, I needed it to work as an "Instance" object).

The new script operates almost like the original. However, these changes make it so you can't just throw a script tag in the body and instantiate the upload object. If you try that IE will have a fit. So you need to attach some code to the window.onload event (or any event that gets executed after the page is finished loading).

Operational Notes/Change log

My installation of Internet Explorer is broken. The SWFObject flash detection does not work for me. I've updated the SWFUpload script so you can set the "flash_version" setting to 0 (zero) bypassing the version detection. I'm not sure what will happen if Flash is not installed.

I've replaced the Debug output with a "Debug Console". The old debug doesn't work now that we have to wait until the page is loaded. The Debug Console looks for a FORM tag, adds a TextArea and puts the output there. If it can't find a FORM then it creates one and adds it to the body.

I've added a setting that allows you to tell SWFUpload where to add the Flash objects. ASP.Net requires that the Flash be added outside the FORM tags.

I've added a setting that indicates where to place the A tag (link) that causes the File Dialog to open.

I've updated the default Error Handler so it writes to the "Debug Console" instead of putting up alerts.

I de-obfuscated a bunch of the SWFObject code. So, now it's easier to read, but makes for a larger file.


The Script


/**
* mmSWFUpload 0.7.1 Revision 2 by Jacob Roberts, January 2007, linebyline.blogspot.com
*
* + Changed from a Static Class to an Instance (changed code/class structure)
* + Added "flash_version" setting. When set to zero the version check is skipped
* + Added Debug Console. The Instance class can't do document.write.
* = De-obfuscated SWFObject a bit
* - Removed standalone mode.
* + Added "ui_target" setting. When non-blank the link is added.
* + Added "flash_target" setting. When blank the flash is appended to the <body> tag
* = This fixes ASP.Net not allowing the flash to be added to the Form
* + Added error checking to the callSWF method
*
* mmSWFUpload 0.7: Flash upload dialog - http://profandesign.se/swfupload/
*
* SWFUpload is (c) 2006 Lars Huring and Mammon Media and is released under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*
* VERSION HISTORY
* 0.5 - First release
*
* 0.6 - 2006-11-24
* - Got rid of flash overlay
* - SWF size reduced to 840b
* - CSS-only styling of button
* - Add upload to links etc.
*
* 0.7 - 2006-11-27
* - Added filesize param and check in SWF
*
* 0.7.1 - 2006-12-01
* - Added link_mode param for standalone links
* if set to "standalone", createElement("a") won't run.
* - Added link_text param if css isn't needed.
* - Renamed cssClass to css_class for consistency
*
*/

/* Constructor */
function mmSWFUpload(settings) {
// Remove background flicker in IE
try {
document.execCommand('BackgroundImageCache', false, true);
} catch(e) {}

this.movieName = "mmSWFUpload_" + mmSWFUpload.movieCount++;

this.init(settings);
this.doDebug = this.getSetting("debug");

this.movieElement = this.loadFlash();
if (this.movieElement == null) {
if (this.doDebug) Console.Writeln("Could not load flash.");
return;
}

this.loadUI();

if (this.doDebug) {
this.debug();
}

}

/* Static thingies */
mmSWFUpload.movieCount = 0;

// Default error handling.
mmSWFUpload.handleErrors = function(errcode, file, msg) {

switch(errcode) {

case -10: // HTTP error
Console.Writeln("Error Code: HTTP Error, File name: " + file.name + ", Message: " + msg);
break;

case -20: // No backend file specified
Console.Writeln("Error Code: No backend file, File name: " + file.name + ", Message: " + msg);
break;

case -30: // IOError
Console.Writeln("Error Code: IO Error, File name: " + file.name + ", Message: " + msg);
break;

case -40: // Security error
Console.Writeln("Error Code: Security Error, File name: " + file.name + ", Message: " + msg);
break;

case -50: // Filesize too big
Console.Writeln("Error Code: File too big, File name: " + file.name + ", File size: " + file.size + ", Message: " + msg);
break;

}

};

/* Instance Thingies */
// Ensure that all the object settings are set or get a default value
mmSWFUpload.prototype.init = function(settings) {
this.settings = [];

this.addSetting("ui_target", settings["ui_target"], "");
this.addSetting("upload_backend", settings["upload_backend"], "");
this.addSetting("upload_start_callback", settings["upload_start_callback"], "");
this.addSetting("upload_complete_callback", settings["upload_complete_callback"], "");
this.addSetting("upload_progress_callback", settings["upload_progress_callback"], "");
this.addSetting("upload_cancel_callback", settings["upload_cancel_callback"], "");
this.addSetting("upload_error_callback", settings["upload_error_callback"], "mmSWFUpload.handleErrors");
this.addSetting("upload_queue_complete_callback", settings["upload_queue_complete_callback"], "");
this.addSetting("allowed_filetypes", settings["allowed_filetypes"], "*.gif;*.jpg;*.png");
this.addSetting("allowed_filesize", settings["allowed_filesize"], "1000");
this.addSetting("debug", settings["debug"], false);
this.addSetting("flash_path", settings["flash_path"], "upload.swf");

this.addSetting("flash_target", settings["flash_target"], "");
this.addSetting("flash_width", settings["flash_width"], "1px");
this.addSetting("flash_height", settings["flash_height"], "1px");
this.addSetting("flash_version", settings["flash_version"], "8")
this.addSetting("flash_color", settings["flash_color"], "#000000");

this.addSetting("link_css_class", settings["link_css_class"], "SWFUploadLink")
this.addSetting("link_text", settings["link_text"], "Upload File");

};

mmSWFUpload.prototype.loadFlash = function() {
var movieElement = null;

// Create SWFObject
var so = new SWFObject(this.getSetting("flash_path"), this.movieName, this.getSetting("flash_width"), this.getSetting("flash_height"), this.getSetting("flash_version"), this.getSetting("flash_color"));

// If we have the right version of flash load the flash
//if (deconcept.SWFObjectUtil.getPlayerVersion(so.getAttribute("version")).major >= this.getSetting("flash_version")) {

so.addParam("wmode", "transparent");
so.addParam("menu", "false");

// Add all settings to flash
so.addVariable("uploadBackend", this.getSetting("upload_backend"));
so.addVariable("uploadStartCallback", this.getSetting("upload_start_callback"));
so.addVariable("uploadProgressCallback", this.getSetting("upload_progress_callback"));
so.addVariable("uploadCompleteCallback", this.getSetting("upload_complete_callback"));
so.addVariable("uploadCancelCallback", this.getSetting("upload_cancel_callback"));
so.addVariable("uploadErrorCallback", this.getSetting("upload_error_callback"));
so.addVariable("allowedFiletypes", this.getSetting("allowed_filetypes"));
so.addVariable("allowedFilesize", this.getSetting("allowed_filesize"));
so.addVariable("uploadQueueCompleteCallback", this.getSetting("upload_queue_complete_callback"));

// Output the flash

so.write(this.getSetting("flash_target"));

movieElement = document.getElementById(this.movieName);

//}

return movieElement;
};

mmSWFUpload.prototype.loadUI = function() {
if(this.getSetting("target") != "") {
var self = this;

// Create link element
var link = document.createElement("a");
link.href = "#";
link.onclick = function () { self.callSWF(); return false; };
link.className = this.getSetting("link_css_class");
link.innerHTML = this.getSetting("link_text");


// Add the link to the target
var target = document.getElementById(this.getSetting("ui_target"))
if (typeof(target) != "undefined" && target && target.innerHTML) {
target.innerHTML = ""; // Clear all the children. This is the "bad way" but I really don't want to walk the childNodes array
target.appendChild(link);
}
}
};

// Make sure that we get a few default values
mmSWFUpload.prototype.addSetting = function(name, value, default_value) {
if (typeof(value) == "undefined" || value == null) {
this.settings[name] = default_value;
} else {
this.settings[name] = value;
}

return this.settings[name];
};

mmSWFUpload.prototype.getSetting = function(name) {
if (typeof(this.settings[name]) == "undefined") {
return null;
} else {
return this.settings[name];
}
};


mmSWFUpload.prototype.callSWF = function() {
if (this.movieElement != null) {
try {
this.movieElement.uploadImage();
}
catch (e) {
if (this.doDebug) {
Console.Writeln("Could not call uploadImage");
}
}
} else {
if (this.doDebug) {
Console.Writeln("Could not find Flash element");
}
}
};

mmSWFUpload.prototype.debug = function() {
var debug_message = "----- DEBUG OUTPUT ----\n";

debug_message += "ID: " + this.movieElement.id + "\n";

// It's bad to use the for..in with an associative array, but oh well
for (var key in this.settings) {
debug_message += key + ": " + this.settings[key] + "\n";
}
/*debug_message += "Flash Target: " + this.getSetting("flash_target") + "\n";
debug_message += "UI Target: " + this.getSetting("ui_target") + "\n";
debug_message += "Upload start callback: " + this.getSetting("upload_start_callback") + "\n";
debug_message += "Upload progress callback: " + this.getSetting("upload_progress_callback") + "\n";
debug_message += "Upload complete callback: " + this.getSetting("upload_complete_callback") + "\n";
debug_message += "Upload filetypes: " + this.getSetting("allowed_filetypes") + "\n";
debug_message += "Max filesize: " + this.getSetting("allowed_filesize") + "kb \n";
debug_message += "Link text: " + this.getSetting("link_text") + "\n";
debug_message += "CSS class: " + this.getSetting("link_css_class") + "\n";
debug_message += "Upload backend file: " + this.getSetting("upload_backend") + "\n";
debug_message += "Upload error callback: " + this.getSetting("upload_error_callback") + "\n";
debug_message += "Upload cancel callback: " + this.getSetting("upload_cancel_callback") + "\n";
debug_message += "Upload queue complete callback: " + this.getSetting("upload_queue_complete_callback") + "\n";*/
debug_message += "----- DEBUG OUTPUT END ----\n";
debug_message += "\n";

Console.Writeln(debug_message);
};

if (typeof Console == "undefined") {
var Console = new Object();
}

Console.Writeln = function(value) {
var console = document.getElementById("mmSWFUpload_Console");

if (!console) {
var documentForm = document.getElementsByTagName("form")[0];

if (!documentForm) {
documentForm = document.createElement("form");
document.getElementsByTagName("body")[0].appendChild(documentForm);
}

console = document.createElement("textarea");
console.id = "mmSWFUpload_Console";
console.style.width = "500px";
console.style.height = "350px";
documentForm.appendChild(console);
}

console.value += value + "\n";

console.scrollTop = console.scrollHeight - console.clientHeight;
}



/**
* SWFObject v1.4: Flash Player detection and embed - http://blog.deconcept.com/swfobject/
*
* SWFObject is (c) 2006 Geoff Stearns and is released under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*
* **SWFObject is the SWF embed script formerly known as FlashObject. The name was changed for
* legal reasons.
*/
if (typeof deconcept == "undefined") {
var deconcept = new Object();
}
if (typeof deconcept.util == "undefined") {
deconcept.util = new Object();
}
if (typeof deconcept.SWFObjectUtil == "undefined") {
deconcept.SWFObjectUtil=new Object();
}

deconcept.SWFObject = function(flash_path, id, width, height, minimum_major_version, background_color, _7, default_quality, _9, _a, _b) {
if (!document.createElement || !document.getElementById) {
return;
}

this.DETECT_KEY = _b ? _b : "detectflash";

this.skipDetect = deconcept.util.getRequestParameter(this.DETECT_KEY) == "0" ? true : false;

this.params = new Object();
this.variables = new Object();
this.attributes = new Array();

if (flash_path) {
this.setAttribute("swf", flash_path);
}

if (id) {
this.setAttribute("id", id);
}

if (width) {
this.setAttribute("width", width);
}

if (height) {
this.setAttribute("height", height);
}

if (minimum_major_version) {
this.setAttribute("version", new deconcept.PlayerVersion(minimum_major_version.toString().split(".")));
} else {
this.setAttribute("version", new deconcept.PlayerVersion([0,0,0]));
}

this.installedVer = deconcept.SWFObjectUtil.getPlayerVersion(this.getAttribute("version"), _7);

if (background_color) {
this.addParam("bgcolor", background_color);
}

var quality = default_quality ? default_quality : "high";

this.addParam("quality", quality);
this.setAttribute("useExpressInstall", _7);
this.setAttribute("doExpressInstall", false);
var _d = (_9) ? _9 : window.location;
this.setAttribute("xiRedirectUrl", _d);
this.setAttribute("redirectUrl", "");

if (_a) {
this.setAttribute("redirectUrl", _a);
}
};

deconcept.SWFObject.prototype = {
setAttribute : function(attribute_name, value) {
this.attributes[attribute_name] = value;
},

getAttribute : function(attribute_name) {
return this.attributes[attribute_name];
},

addParam: function(name, value){
this.params[name] = value;
},

getParams: function() {
return this.params;
},

addVariable: function(name, value) {
this.variables[name] = value;
},

getVariable: function(name) {
return this.variables[name];
},

getVariables: function(){
return this.variables;
},

getVariablePairs: function() {
var name_value_pairs = new Array();
var variables = this.getVariables();

for (var key in variables) {
name_value_pairs.push(key + "=" + variables[key]);
}

return name_value_pairs;
},

getSWFHTML : function() {
var html = "";

// Create Mozilla Embed HTML
if (navigator.plugins && navigator.mimeTypes &amp;& navigator.mimeTypes.length) {
if (this.getAttribute("doExpressInstall")) {
this.addVariable("MMplayerType","PlugIn");
}

// Build the basic embed html
html = '<embed type="application/x-shockwave-flash" src="' + this.getAttribute("swf") + '" width="' + this.getAttribute("width") + '" height="' + this.getAttribute("height") + '"';
html += ' id="' + this.getAttribute("id") + '" name="' + this.getAttribute("id") + '" ';

// Add all our parameters
var params = this.getParams();
for (var key in params) {
html += [key] + '="' + params[key] + '" ';
}

// Add the flash variables, this is passed as a query string (e.g. name=value&name=value) via the "flashvars" attribute
var variable_pairs = this.getVariablePairs().join("&amp;");
if (variable_pairs.length > 0) {
html += 'flashvars="' + variable_pairs + '"';
}

html += "/>";

// Create IE Object HTML
} else {

if (this.getAttribute("doExpressInstall")) {
this.addVariable("MMplayerType","ActiveX");
}

// Build the basic Object tag
html = '<object id="' + this.getAttribute("id") + '" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="' + this.getAttribute("width") + '" height="' + this.getAttribute("height") + '">';
html += '<param name="movie" value="' + this.getAttribute("swf") + '" />';

// Add our parameters
var params = this.getParams();
for(var key in params) {
html += '<param name="' + key + '" value="' + params[key] + '" />';
}

// Add the flash variables. This is passed as a query string (e.g. name=value&name=value) via the "flashvars" param
var variable_pairs = this.getVariablePairs().join("&amp;");
if (variable_pairs.length > 0) {
html += '<param name="flashvars" value="' + variable_pairs + '" />';
}

html += "</object>";
}

return html;
},

write : function(flash_target) {

if (this.getAttribute("useExpressInstall")) {

var min_version_for_express_install = new deconcept.PlayerVersion([6,0,65]);

if (this.installedVer.versionIsValid(min_version_for_express_install) && !this.installedVer.versionIsValid(this.getAttribute("version"))) {
this.setAttribute("doExpressInstall", true);
this.addVariable("MMredirectURL", escape(this.getAttribute("xiRedirectUrl")));
document.title = document.title.slice(0,47) + " - Flash Player Installation";
this.addVariable("MMdoctitle", document.title);
}
}

if (this.skipDetect || this.getAttribute("doExpressInstall") || this.installedVer.versionIsValid(this.getAttribute("version"))) {
// Attempt to load the flash appended to the body

// NOTE: this part will cause IE to throw an error if not called after the page is finished loading
var container = document.createElement("div");
container.style.width = "0px";
container.style.height = "0px";
container.style.position = "absolute";
container.style.top = "0px";
container.style.left = "0px";

var target_element;
if (flash_target != "") {
target_element = document.getElementById(flash_target);
}
if (typeof(target_element) == "undefined") {
target_element = document.getElementsByTagName("body")[0];
}
if (typeof(target_element) == "undefined") {
return false;
}

target_element.appendChild(container);

container.innerHTML = this.getSWFHTML();

return true;
} else {
if (this.getAttribute("redirectUrl") != "") {
document.location.replace(this.getAttribute("redirectUrl"));
}
}

return false;
}
};

deconcept.SWFObjectUtil.getPlayerVersion = function(_23,_24) {
var player_version = new deconcept.PlayerVersion([0,0,0]);

// Mozilla Detect
if (navigator.plugins && navigator.mimeTypes.length) {
var swf_plugin = navigator.plugins["Shockwave Flash"];
if (swf_plugin && swf_plugin.description) {
player_version = new deconcept.PlayerVersion(swf_plugin.description.replace(/([a-z]|[A-Z]|\s)+/,"").replace(/(\s+r|\s+b[0-9]+)/,".").split("."));
}

// IE Detect
} else {
try {
var axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
for(var i=3; axo != null; i++) {
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash." + i);
player_version = new deconcept.PlayerVersion([i,0,0]);}
} catch (e){ }

if (_23 && player_version.major > _23.major) { return player_version; }

if (!_23 || ((_23.minor != 0 || _23.rev != 0) && player_version.major == _23.major) || player_version.major != 6 || _24) {
try{
player_version = new deconcept.PlayerVersion(axo.GetVariable("$version").split(" ")[1].split(","));
}
catch (e) { }
}
}

return player_version;
};

deconcept.PlayerVersion = function(version_array) {
this.major = parseInt(version_array[0]) != null ? parseInt(version_array[0]) : 0;
this.minor = parseInt(version_array[1]) || 0;
this.rev = parseInt(version_array[2]) || 0;
};

deconcept.PlayerVersion.prototype.versionIsValid = function(fv) {
if (this.major < fv.major) { return false; }
if (this.major > fv.major) { return true; }
if (this.minor < fv.minor) { return false; }
if (this.minor > fv.minor) { return true; }
if (this.rev < fv.rev) {return false; }

return true;
};

deconcept.util = {
getRequestParameter : function(parameter_name) {
var query_string = document.location.search || document.location.hash;
if (query_string) {
var parameter_pair = query_string.indexOf(parameter_name + "=");
var parameter_value_end_index = (query_string.indexOf("&", parameter_pair) > -1) ? query_string.indexOf("&", parameter_pair) : query_string.length;
if (query_string.length > 1 && parameter_pair > -1) {
return query_string.substring(query_string.indexOf("=", parameter_pair) + 1, parameter_value_end_index);
}
}

return "";
}
};

if (Array.prototype.push == null) {
Array.prototype.push = function(_2f) {
this[this.length] = _2f;
return this.length;
};
}
var getQueryParamValue = deconcept.util.getRequestParameter;
var FlashObject = deconcept.SWFObject; // for backwards compatibility
var SWFObject = deconcept.SWFObject;




Example HTML



<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>SWFUpload ASP.Net Revision</title>
<script type="text/javascript" src="jscripts/SWFUpload/mmSWFUploadR2.js"></script>
<script type="text/javascript">
uploadStart = function(fileObj) {
try {
Console.Writeln("Upload started");
} catch (e) { Console.Writeln("Error displaying file upload start information"); }

}

uploadProgress = function(fileObj, bytesLoaded) {

try {
var percent = Math.ceil((bytesLoaded / fileObj.size) * 100)
Console.Writeln("Upload Progress: " + fileObj.name + " " + percent);
} catch (e) { Console.Writeln("Error displaying file progress"); }
}

uploadComplete = function(fileObj) {
try {
Console.Writeln("Upload Complete: " + fileObj.name);
} catch (e) { Console.Writeln("Error displaying file upload complete information"); }
}

uploadQueueComplete = function(fileObj) {
try {
Console.Writeln("Queue Done");
} catch (e) { Console.Writeln("Error displaying queue complete information"); }
}

uploadCancel = function() {
try {
Console.Writeln("Pressed Cancel");
} catch (e) { Console.Writeln("Error displaying file cancel information"); }
}
</script>

<style type="text/css">

a.SWFUploadLink {
font-weight: bold;
color: blue;
text-decoration: none;
}

a.SWFUploadLink:hover {
text-decoration: underline;
}

</style>



</head>
<body>
<div id="flashTarget"></div>
<form action="" onsubmit="return false;">

<div id="wrapper">

<div id="SWFUpload">
Upload a PNG: <input type="file" name="upload" />
</div>

<div id="SWFUpload2">
Upload a GIF: <input type="file" name="upload" />
</div>


<input type="submit" value="Upload" onclick="javascript:alert('disabled...'); return false;" />



<script type="text/javascript">
var upload1;
var upload2;
var da_onload = window.onload;
window.onload = function() {
if (typeof(da_onload) == "function") {
da_onload();
}

upload1 = new mmSWFUpload({
debug : true,
flash_version: 8,
upload_backend : "../../upload.aspx",
ui_target : "SWFUpload",
flash_target : "flashTarget",
link_text : "Select File",
//link_css_class : "myCustomClass",
allowed_filesize : "400",
allowed_filetypes : "*.png",
upload_start_callback : 'uploadStart',
upload_progress_callback : 'uploadProgress',
upload_complete_callback : 'uploadComplete',
// upload_error_callback : 'uploadError',
upload_cancel_callback : 'uploadCancel',
upload_queue_complete_callback : 'uploadQueueComplete'
});

upload2 = new mmSWFUpload({
debug : true,
flash_version: 8,
upload_backend : "../../upload.aspx",
ui_target : "SWFUpload2",
flash_target : "flashTarget",
link_text : "Select File",
//link_css_class : "myCustomClass",
allowed_filesize : "400",
allowed_filetypes : "*.png",
upload_start_callback : 'uploadStart',
upload_progress_callback : 'uploadProgress',
upload_complete_callback : 'uploadComplete',
// upload_error_callback : 'uploadError',
upload_cancel_callback : 'uploadCancel',
upload_queue_complete_callback : 'uploadQueueComplete'
});
}


</script>


</div>
</form>

</body>
</html>



Usage Tips

Here are some tips on using this thing:
  • Refer to the script's init method to see all the settings and their defaults
  • Any settings you don't include will automatically get the default value
  • Any callbacks you don't specify won't be called
  • Setting flash_version to 0 (zero) disables the flash version detection
  • You can use the Debug Console for your own output, if you want
  • The upload_backend setting should have a comlpete URL or be relative to the SWF file (not the Javascript or the HTML file)
  • If you don't specifiy a ui_target setting (or the target is not found) then the Upload link is not added.
  • You can call up the File Dialog yourself using the callSWF() method on your SWFUpload instance objects.
  • If you don't specify a flash_target setting the script adds the flash to the body (which will be outside any FORMs).
  • In your events the this object won't be your SWFUpload instance object.
  • You can't attach the callbacks like you do Javascript events. The callback string has to be the full string you would use if you were calling the callback from Javascript. P.S. Callbacks are just Javascript functions you've declared somewhere. See the example callbacks to see what values are passed in when they are called.
  • SWFUpload doesn't seem to work if you are viewing the page as a local file (file://). You need to put it on a web server (http://)
  • SWFUpload will probably return an error if your upload_backend is not a script file (if you submit to an html/htm file IIS will return a HTTP 405 error. Submit to a script file (php, aspx, etc).
  • Make sure you upload_backend file is working normally before using it with SWFUpload. You will get IOErrors or HTTP errors.
  • For ASP.Net developers you'll want to make sure your maxRequestLength and executionTimeout are large enough.

So, there you go. Again, Good Luck!

Monday, December 18, 2006

SWFUpload

There is a fundamental problem with File Uploads in web browsers. If there is anything that is going to cause the file to be invalid you can't let the user know until after they have uploaded the entire file.

"Life Sucks" Rant
If the file is of the wrong type, wrong size, or doesn't meet some of other criteria it doesn't matter. The browser happily sends the complete file to the web server. Then you can check out the file and return some sort of status message.

Great! My user, in his remote village (I live in Alaska) gets on his Dial-Up, or uses the slow satellite connection at the local community center. He attaches his 20MB resume, waits 20 minutes while it uploads and then gets a nice message back saying: "The file was too big. You just burned away 20 minutes of your life. Would like to burn away another 20? Please try again."

Wouldn't it be great if the browser could let me know a little bit about the file before it's uploaded? The browser allows Javascript to see the full file path. Would it risk national security to give me the file size?

I've looked in to a few solutions. We are an ASP.Net shop and I checked out a couple controls that do file upload progress and file size check, etc. They are nifty but don't really cut it. They are too fragile. And I don't want to fix one problem while introducing a dozen new ones.

It turns out that since Flash 8 you can do uploads via the browsers Flash Plugin and so I discovered SWF Upload. It's pretty nice and avoids some of the pitfalls that the other "browser only" solutions have to face.

I've been trying to integrate the thing with ASP.Net 2.0. There are some issues. I'm pretty proficient at Javascript and worked around some bugs. It seemed to be going well. Now I just have to deal with the issue of having a separate page hit for receiving the files and tying that back to the form the user is completing right now.

Everything seemed rosy. However, today, the thing just stopped working and always returns an IOError. Even the unmodified sample code stopped working.

Oh well. I guess I'll just wait until the browser vendors fix the File control.


Getting SWFUpload to work in ASP.Net

SWFUpload returns an error if you try to wedge the thing inside the <form runat="server"> tags. This has to do with the way things get arranged inside the DOM when the Flash is dynamically added to the page.

My solution was to change the Javascript that SWFUpload uses to add itself to the page. Instead of adding itself in the "target" it needs to add itself to the "body".

This can be done by modifying the mmSWFUpload.js file. Find the line:

n.innerHTML = this.getSWFHTML();

in the "write" method and replace it with this:

var body = document.getElementsByTagName("body")[0];
var container = document.createElement("div");
body.appendChild(container);
container.innerHTML = this.getSWFHTML();

There you go. That'll do it. The SWFUpload flash object is added outside the FORM tags and works correctly. Good luck with the rest.

Wednesday, November 22, 2006

The Drain That Wouldn't Drain

Since being married (2000) we have lived in 4 homes. Each has had its quirks. But each seemed to have issues with the bathtub drain to some extent. Some tubs were worse than others. I think my wife took care of some while I was at work (she mentioned pliers). They all seemed to drain...um...less well...after awhile. I don't remember this happening when I was a kid. But I was pretty oblivious to everything then.

Our "new"(1973) house has the same problem. We've only lived here for 6 months. We replaced the drain and some pipes below the tub when we moved in. But now it already takes half the day to drain the tub. It fills with slimy hairy soap water when you shower and leaves and disgusting mess when it finally drains.

So, I'm off to see the Google to find out how to get this tub to drain again. In the past we've tried chemicals (we even had a plumber apply his super powerful chemicals). We tried coat hangers. We tried some sorry excuse for a snake that we picked up at the dollar store. All these things helped, a bit, but not really.

So, Google had lots of interesting information, most of which we had tried. But one Blog (which I can't find again) mentioned a nifty tool called "Zip-It". It's a long slender flexible 2-foot plastic stick with 1/4" barbs on it. They claim that all you have to do is stick it down the drain and pull it out again. It pulls out all the hair and attached gunk clearing the drain. The Blog mentioned that they were able to buy some at Lowes.

So, I checked out the website. Impressive isn't it. But it's only a few bucks and we haven't tried it yet.

I wish I would have pulled out the digital camera so you all could see the dirty glory. The Zip-It (about $3 at Lowes) pulled out so much nasty that I thought I'd be sick. The box says that you can only use it once or to throw it away after use or some such. But after one use I could tell there was a lot more crap down there (Our drain was twice as bad and gross as the one in the video on the Zip-It websit). So I cleaned the thing off and pushed it down 4 or 5 more times until there wasn't anything left to pull up. I did have to get some scissors to cut the hair off that wrapped around our drain (battling for survival it seemed). But the drain drains! It's like new!! Hurrah! We conquered!

I fully recommend the Zip-It for hair related clogs. But now my feet get cold when I shower. I kinda miss that warm backed-up water. Que sera.

Here's an image from the Zip-It website that about matches what we pulled up on our first "zip"

Friday, November 10, 2006

Textarea Cursor Position in Internet Explorer

Background...
I'm building a content management type website and wanted to include a nice WYSIWYG editor. Unfortunately all the editors I tested are either too unpredictable and erratic, cost a fortune, or are not customizable enough for my needs (out of the box anyways).

In the end I just didn't want to deal with the awful (x)html that they produce. I also didn't want to deal with my end users, whom have never heard of HTML and won't want to "View Source" to fix the HTML when the WYSIWYG fails.

So, I decided to build my own. After a smidgen of research and testing I decided not to build my own. Now I'm going for a Wiki style code editor with a toolbar. With this I can generate clean consistent simple markup. It's not WYSIWYG but at least it will be consistent and predictable.

I quickly found that I'd need to be able to retrieve and adjust the current text selection in the textarea control. So, after not finding any satisfactory solutions via Google I've come up with my own.

Textarea Cursor Position in IE

First off I'd like to give credit to the sites that provided good tips, suggestions, and solutions.
  • parentNode - The best written and closest solution I found.
  • Mishoo - A good idea but the comments were more helpful
  • Scott Van Vliet - Also a very interesting approach. The comments were also helpful
  • Thanks to WikkaWiki which has a good solution for their text editor.
  • MSDN Library
Summary of the Implementation

My solution works like this:
  1. Make 2 TextRanges.
    1. The first TextRange selects from the beginning of the textarea to the beginning of the selected text.
    2. The second TextRange is a duplicate of the selection.
  2. Restore missing new line characters in both TextRanges
    1. The TextRange.text property right trims all the \r\n characters from the selection. We must restore the new lines to calculate the correct cursor position.
  3. Calculate the start position which is the untrimmed length of the 1st TextRange.text property.
  4. Calculate the end position which is the untrimmed length of the 2nd TextRange.text property plus the start position.
  5. Calculate the complete untrimmed selection using textarea.value.substring(startPoint, endPoint).
Further Explanation and Repetition

Internet Explorer makes this whole process very difficult. The biggest issues are:
  • It treats \r\n as one character when working with the TextRanges
  • It trims \r\n from the end of the TextRange.text so you don't know the real selection length.
My solution discovers the correct index of the start and end positions of the selection which can't be done by just using the TextRange.text.length. Once you have the positions you can use textarea.value.substring(start,end) to get the actual selected text rather than the trimmed TextRange.text property.

All the string methods (substring, indexOf, length, etc.) treat \r\n as 2 characters, however, the TextRange methods (moveStart, moveEnd, etc.) treat \r\n as 1 character. So just moving the TextRange until we reach the beginning and counting how many times we moved will be off by exactly the number of new lines passed along the way.

A Word About Changing the Selection

The TextRange.text \r\n trim issue also causes problems when attempting to change the selection's position. You need to count the newlines between the beginning and the target index and then subtract count from the target index. Then you can use this adjusted index with the TextRange.moveStart and TextRange.moveEnd methods to get the correct selection.

The Code

In my code I use 3 TextRanges (instead of 2 as described above) so that I can re-assemble the entire string and compare it to the original textarea.value. I do this to make sure I performed the untrimming correctly.

You will also notice that I only use a single do..while loop to untrim all the TextRanges. I could have done this with 3 separate loops. Even better, I probably should move the untrim bit to a separate function. I purposely made these design decisions but it could be done differently.

Also note that this bit of code was only tested in IE 6.0. Please don't complain because it doesn't work in Opera, Firefox, Safari or Lynx.


var textarea = document.getElementById("myTextArea");
textarea.focus();
var selection_range = document.selection.createRange().duplicate();

if (selection_range.parentElement() == textarea) {    // Check that the selection is actually in our textarea
  // Create three ranges, one containing all the text before the selection,
  // one containing all the text in the selection (this already exists), and one containing all
  // the text after the selection.
  var before_range = document.body.createTextRange();
  before_range.moveToElementText(textarea);                    // Selects all the text
  before_range.setEndPoint("EndToStart", selection_range);     // Moves the end where we need it

  var after_range = document.body.createTextRange();
  after_range.moveToElementText(textarea);                     // Selects all the text
  after_range.setEndPoint("StartToEnd", selection_range);      // Moves the start where we need it

  var before_finished = false, selection_finished = false, after_finished = false;
  var before_text, untrimmed_before_text, selection_text, untrimmed_selection_text, after_text, untrimmed_after_text;

  // Load the text values we need to compare
  before_text = untrimmed_before_text = before_range.text;
  selection_text = untrimmed_selection_text = selection_range.text;
  after_text = untrimmed_after_text = after_range.text;

  // Check each range for trimmed newlines by shrinking the range by 1 character and seeing
  // if the text property has changed.  If it has not changed then we know that IE has trimmed
  // a \r\n from the end.
  do {
    if (!before_finished) {
      if (before_range.compareEndPoints("StartToEnd", before_range) == 0) {
        before_finished = true;
      } else {
        before_range.moveEnd("character", -1)
        if (before_range.text == before_text) {
          untrimmed_before_text += "\r\n";
        } else {
          before_finished = true;
        }
      }
    }
    if (!selection_finished) {
      if (selection_range.compareEndPoints("StartToEnd", selection_range) == 0) {
        selection_finished = true;
      } else {
        selection_range.moveEnd("character", -1)
        if (selection_range.text == selection_text) {
          untrimmed_selection_text += "\r\n";
        } else {
          selection_finished = true;
        }
      }
    }
    if (!after_finished) {
      if (after_range.compareEndPoints("StartToEnd", after_range) == 0) {
        after_finished = true;
      } else {
        after_range.moveEnd("character", -1)
        if (after_range.text == after_text) {
          untrimmed_after_text += "\r\n";
        } else {
          after_finished = true;
        }
      }
    }

  } while ((!before_finished || !selection_finished || !after_finished));

  // Untrimmed success test to make sure our results match what is actually in the textarea
  // This can be removed once you're confident it's working correctly
  var untrimmed_text = untrimmed_before_text + untrimmed_selection_text + untrimmed_after_text;
  var untrimmed_successful = false;
  if (textarea.value == untrimmed_text) {
    untrimmed_successful = true;
  }
  // ** END Untrimmed success test

  var startPoint = untrimmed_before_text.length;
  var endPoint = startPoint + untrimmed_selection_text.length;
  var selected_text = untrimmed_selection_text;

  alert("Start Index: " + startPoint + "\nEnd Index: " + endPoint + "\nSelected Text\n'" + selected_text + "'");
}