Posted in Development on Wednesday, Wednesday, May 06, 2009 by Anthony Burns
The next step in this series is to parse the Xml data returned from the Twitter API into an array of status objects, then bind them to the table in our main window.
The article I referenced in the first post shows how to use the cocoa class NSXMLDocument and its friends to parse an xml document:
http://cocoawithlove.com/2008/09/cocoa-application-driven-by-http-data.html
Putting that together with the NSXMLElement docs at Apple:
http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Classes/NSXMLElement_Class/Reference/Reference.html
And some XPath documentation courtesy of W3Schools:
http://www.w3schools.com/XPath/xpath_syntax.asp
We're armed with enough knowledge to start parsing the Twitter Xml response.
In our app we're currently mapping an array of Friend objects (first_name, last_name) to the table in the main window, so we can just as easily map an array of TwitterStatus objects to the table with very little change in code.
First we'll create a TwitterStatus class with accessors for the various fields. Although we'll only be dealing with the users' names and statuses in this article, we'll take the opportunity to grab the users' profile image while we're here for the next article.
class TwitterStatus attr_accessor :text, :screen_name, :profile_image_url end
Then using the example from the Cocoa With Love article and the Apple docs as a starting point, we can write the following method for our TwitterApi class to parse the Xml returned from Twitter into an array of our TwitterStatus objects.
def parseXml(responseData) if not responseData then return end err = nil # Create an XMLDocument object from the Xml string begin document = NSXMLDocument.alloc.initWithData responseData, options:NSXMLDocumentTidyXML, error:err rescue StandardError => e alert(e.to_s) return end # Create arrays of all the elements we want extracting from the Xml using XPath rootNode = document.rootElement statusTexts = rootNode.nodesForXPath "//status/text", error:err userScreenNames = rootNode.nodesForXPath "//status/user/screen_name", error:err userProfileImages = rootNode.nodesForXPath "//status/user/profile_image_url", error:err result = [] statusTexts.length.times do |i| # Iterate over every status and populate a new TwitterStatus object with them status = TwitterStatus.new status.text = statusTexts[i].stringValue status.screen_name = userScreenNames[i].stringValue status.profile_image_url = userProfileImages[i].stringValue result << status end return result end
Next a quick change to the timeline method so we parse the Xml and hand the resulting array to the callback.
def getTimeline(callback)
getUrl('http://twitter.com/statuses/friends_timeline.xml',
true, lambda {|data| callback.call(self.parseXml(data)) })
end
Which we can then consume from our refresh button like so:
@twitter.getTimeline(lambda do |data| @friends = data @friendsTableView.reloadData end)
The final thing to do to make the whole thing work is to tweak the tableView method to return the correct properties from the TwitterStatus objects.
def tableView(view, objectValueForTableColumn:column, row:index) friend = @friends[index] case column.identifier when 'screen_name' friend.screen_name when 'text' friend.text end end
Now hitting the refresh button should fill the table with your 20 most recent updates from Twitter. If it doesn't, then you've done something wrong. Don't blame me.
Someone asked in the comments of one of the previous articles if I could post the whole code or upload to github. I'll do that after I post the next article in about a week.
Speaking of which, the next article will show how to download the users' profile images and use them in the table instead of their name.
Tagged as: cocoa, macruby, ruby
Posted in Development on Sunday, Sunday, April 26, 2009 by Anthony Burns
UPDATE: Part Three of this series is now available
Adding Basic Authentication to a url request is pretty straight forward, in fact it only requires a header to be added to the request with the NSURLRequest.addValue:forHTTPHeaderField method. Simple huh? Not quite. The HTTP spec requires that the username and password be Base64 encoded, which is usually a case of requiring the base64 library in standard Ruby, but that isn't available in MacRuby, and there doesn't appear to be Cocoa object designed for the purpose.
After spending five minutes googling turned up exactly bot all, I decided to change tack and find out how to base64 encode myself.
How to Base64 Encode
http://email.about.com/cs/standards/a/base64_encoding.htm
So with a combination of the above article, a trusty Ruby book, blood, sweat, tears and a lot of trial and error I constructed:
class Base64
ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-="
def encode(str)
dat = []
str.each_byte { |b| dat << b }
# Append 0s to the data to make it's length divisible by 3
missingChars = 3 - (dat.length % 3)
missingChars.times { dat << 0 }
result = ''
sections = dat.length / 3
sections.times do |i|
a = dat[(i * 3) + 0] >> 2
b = dat[(i * 3) + 0] << 4 & 63
b = b | dat[(i * 3) + 1] >> 4
c = dat[(i * 3) + 1] << 2 & 63
c = c | dat[(i * 3) + 2] >> 6
d = dat[(i * 3) + 2] & 63
if dat[(i * 3) + 1] == 0 then c = 64 end
if dat[(i * 3) + 2] == 0 then d = 64 end
result += ALPHA[a] + ALPHA[b] + ALPHA[c] + ALPHA[d]
end
return result
end
end
There's no point me stepping through it, you should be able to dissect it easy enough with the referenced article, or just use it if you don't care how it works - it's not perfect and probably won't work in all scenarios, but it's good enough for Base64 encoding a username/password combination.
We can then use the Base64 class to encode our credentials and add the Basic Authentication header, which I've chosen to do in a separate method of the TwitterApi class called addAuthentication:
def addAuthentication(req) enc = Base64.new data = @username + ':' + @password data = enc.encode(data) data = 'Basic ' + data req.addValue data, forHTTPHeaderField:'Authorization' end
I've also added a couple of attributes to the class that allow the username and password to be set:
attr_accessor :username, :password
And I've added a call to addAuthentication to the getUrl method as well as an authenticated parameter to the method signature, allowing you to specify if you want the request to be authenticated or not:
def getUrl(urlString, authenticated, delegate) callback = HttpRequestCallback.new callback.delegate = delegate callback.buf = NSMutableData.new callback.response = nil url = NSURL.URLWithString(urlString) request = NSMutableURLRequest.requestWithURL(url, cachePolicy:POLICY, timeoutInterval:TIMEOUT) addAuthentication(req) if authenticated # <-- Add Authentication if required callback.conn = NSURLConnection.alloc.initWithRequest(request, delegate:callback) end
Finally I've added a getTimeline method to the class that calls getUrl for the authenticated user's timeline:
def getTimeline(callback)
getUrl('http://twitter.com/statuses/friends_timeline.xml', true, lambda {|data| callback.call(data) })
end
We can consume this like so:
twitter = TwitterApi.new twitter.username = 'xxx' twitter.password = 'xxx' twitter.getTimeline(lambda do |data| result = NSString.alloc.initWithData data, encoding:NSUTF8StringEncoding # display the result in an alert box alert = NSAlert.new alert.setMessageText(result) alert.runModal() end)
Which should download the Xml of your authenticated Twitter timeline and display it in a message box.
So we can now obtain our own timeline authenticated using Basic Authentication; next we'll parse the Xml into an array of status objects and bind them to the table in our main window. Part Three Available Here.
Tagged as: cocoa, macruby, ruby
Posted in Development on Sunday, Sunday, April 19, 2009 by Anthony Burns
UPDATE: Part Two of this series is now available
Howdy. This is a new series I intend to write on how to create a Twitter client in MacRuby using the Cocoa framework on OSX. There doesn't seem to be a lot of articles around on this subject, so I thought it would be beneficial to the world at large if I chronicled my own journey.
Warning: this is not a "Learning Ruby" guide, if you don't know how to program in Ruby, or have a good Ruby book at hand, then you ain't gonna get too far with this.
I first started by following the original Getting Started with MacRuby tutorial provided by Apple, which, as well as giving you a good introduction to MacRuby, gets you to the starting point for this series - we'll adapt the application you build following that tutorial into our Twitter client, so off you go and follow that. We'll see you back here in half an hour.
---
Welcome back. Now that you've followed the Apple tutorial, you should have an application that contains a table and a button - the next step, and the purpose of this first article, is to add the ability to download data from a url.
There are easy ways to do synchronous url requests, such as NSString.initWithContentsOfURL, but synchronous calls would freeze our UI while we wait for the call to complete, which is bad for the user, so we need to asynchronously request our urls.
A bit of googling found me an article in Objective-C that taught me everything we need to do to achieve our goal,
unfortunately it was in Objective-C:
http://cocoawithlove.com/2008/09/cocoa-application-driven-by-http-data.html
Luckily, after searching on the methods used in the Objective-C article, I managed to find some MacRuby code that
implemented async url requests, so saved myself a big Cocoa->Ruby headache:
http://github.com/psychs/limechat/blob/4c8a75aff8f3f7af10bf1e6049baeee29af7d82b/ruby/lib/pasternakclient.rb
The NSURLConnection.initWithRequest method requires a delegate be provided that meets certain criteria. The request method will immediately return and the actual request will be performed in the background - once the request completes, it will hand the result to the code in this delegate object.
Both the Obj-C and MacRuby examples passed 'self' in as that delegate, but that would limit our object to only being able to run one request at a time. Therefore I knocked up the following class to be the delegate we pass, which allows us to set a delegate for each request:
class HttpRequestCallback attr_accessor :delegate, :buf, :response, :conn def cancel if @conn @conn.cancel @conn = nil end end def connection(conn, didReceiveResponse:res) return if @conn != conn @response = res end def connectionDidFinishLoading(conn) if @response code = @response.statusCode if code.to_s =~ /^20[01]$/ @delegate.call(@buf) else @delegate.call(false) end end @conn = nil end def connection(conn, didReceiveData:data) return if @conn != conn @buf.appendData(data) end def connection(conn, didFailWithError:err) if @conn == conn @delegate.call(false) end @conn = nil end def connection(conn, willSendRequest:req, redirectResponse:res) return nil if @conn != conn if res && res.statusCode == 302 @delegate.call(req.URL.to_s) @conn = nil nil else req end end end
The idea being that we create one of these each time we request a url, the request then communicates with this class which in turn calls a method of our choosing when the request is complete. Think of it like a middle man: We want the request to call one of our methods when it completes, however, the NSUrlConnection class requires a bit of infrastructure in place to deal with things, so this class is the middleman who has the complicated conversation with NSUrlConnection and then simply hands us an envelope with the answer.
With the HttpRequestCallback class in place, we can now implement the following getUrl method in a TwitterApi class:
class TwitterApi TIMEOUT = 10 POLICY = NSURLRequestReloadIgnoringLocalCacheData def getUrl(urlString, delegate) callback = HttpRequestCallback.new callback.delegate = delegate callback.buf = NSMutableData.new callback.response = nil url = NSURL.URLWithString(urlString) request = NSMutableURLRequest.requestWithURL(url, cachePolicy:POLICY, timeoutInterval:TIMEOUT) callback.conn = NSURLConnection.alloc.initWithRequest(request, delegate:callback) end end
We call this method passing it a url string and a lambda method that will be called when the request returns. The lambda will be called with one parameter of type NSMutableData, which we can convert into a string using NSString.initWithData. We can therefore consume and test our getUrl method by replacing the code in the addFriend method with:
def addFriend(sender)
twitter = TwitterApi.new
twitter.getUrl('www.google.com', lambda do |data|
result = NSString.alloc.initWithData data, encoding:NSUTF8StringEncoding
# display the result in an alert box
alert = NSAlert.new
alert.setMessageText(result)
alert.runModal()
end)
end
Now when you run your application and click on the Add button, you should get a message alert containing the HTML source for the Google homepage.
Next Article: We need to be able to authenticate ourselves with the Twitter API in order to get access to our own Twitter stream - so we'll cover Basic Authentication and Base64 encoding. Part Two Available Here.
Additional MacRuby Tutorial - makes understanding Objective-C syntax easier:
http://www.macruby.org/trac/wiki/MacRubyTutorial
Apple Objective-C Messaging:
https://developer.apple.com/documentation/Cocoa/Conceptual/ObjectiveC/Articles/chapter_2_section_4.html
Apple reference for NSUrlReq:
http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSMutableURLRequest_Class/Reference/Reference.html
Article on using NSUrlReq in Objective-C:
http://cocoawithlove.com/2008/09/cocoa-application-driven-by-http-data.html
The Twitter API Documentation:
http://apiwiki.twitter.com
Tagged as: cocoa, macruby, ruby
Posted in Development on Saturday, Saturday, February 07, 2009 by Anthony Burns
Howdy Quakk fans.
It seems the nice folks at Twitter made a change to their API a couple of months ago that caused all applications built with the .NET framework to fail when posting status updates. Unfortunately I've been too busy with another Twitter related project (details below) to fix Quakk, until now. You can download and install the fixed version from the Quakk webpage.
Now that we've got that over and done with, I'd like to introduce you to Tweeter Tags. It's an idea that's been created by a team of four people who met over Twitter, and we built it in less than a month. It allows you to assign tags to your Twitter profile, so other folk with similar interests can find you, and you can find them.
If you're into photography and you want to find people on Twitter that are into it too, simply sign up, add the photography tag to your profile and then click the tag to see who else has tagged themselves with it. If you're having a problem with SQL Server and want to find some SQL bods to grill about it on Twitter, then click the SQL tag and we'll show you who to ask.
For more details check out the Tweeter Tags Blog or if I've sold the idea to you already, then get yourself straight to the Tweeter Tags website.
Tagged as: quakk, tweetertags
Posted in Development on Saturday, Saturday, October 18, 2008 by Anthony Burns
I'm digging the new ASP.NET MVC framework. Big style. Transitioning from classic ASP to ASP.NET left me a bit frustrated with the abstraction introduced. It was supposed to be better for us developers that the stateless nature of HTTP was being hidden away, that the framework would take care of all the state management, leaving us time to eat pizza and drink beer; but that just left me fat and drunk. Thankfully I can now dive back into having full control of my html and post data, among other things, by using MVC.
However, I don't want to hand craft every single piece of html every time, and there's a lot of things day to day that I just want rendering without a lot of effort - tables being one of them, so I set about building a simple but relatively powerful table control.
Requirements: I wanted the table to render an IEnumerable I might pull straight from a Linq query to a html table; including the ability to make each row a link, to highlight a selected row and the facility to add columns to the end of the table containing action links such as "Delete".
After Googling 'asp net mvc controls' I discovered a few different ways of creating and rendering controls under the MVC framework. I finally opted to write an extension method to the HtmlHelper class, made available through the Html property on each MVC View.
Since this probably won't be the last control I'll build, I created an interface each control could implement:
interface ISimpleControl {
string Render(HtmlHelper helper)
}
I then created a small HtmlHelper extension method to render these simple controls:
public static string RenderControl(this HtmlHelper helper, ISimpleControl control)
{
return control.Render(helper);
}
I can now render any simple control using:
<%=Html.RenderControl(new MyFirstControl());%>
Because my datasource could be any type of object returned from a Linq query (including anonymous types) I have to use reflection to discover the properties of these objects in order to render the columns of the table. I opted to do this as soon as the data source is set and convert it to a DataTable for easy traversal when it comes to rendering the html:
private static DataTable GetDataTableFromIEnumerable(IEnumerable dataSource)
{
// Since IEnumerable doesn't expose an indexer to get the first element
object obj;
foreach (object o in data)
{
obj = o;
break;
}
if (obj == null) return null;
DataTable dataTable = new DataTable();
// Use reflection to create DataColumns for each of the properties in the object
Type type = obj.GetType();
PropertyInfo[] propertyInfo = type.GetProperties();
string[] keys = new string[propertyInfo.Length];
for (int i = 0; i < keys.Length; i++)
{
DataColumn column = new DataColumn(propertyInfo[i].Name, typeof(string));
dataTable.Columns.Add(column);
keys[i] = propertyInfo[i].Name;
}
// Iterate through the IEnumerable and populate the DataTable from
// each object using Reflection
foreach (object row in dataSource)
{
DataRow dataRow = dataTable.NewRow();
foreach (DataColumn column in dataTable.Columns)
{
object result = type.InvokeMember(column.ColumnName, BindingFlags.GetProperty,
null, row, null);
if (result != null)
{
dataRow[column] = result.ToString();
}
}
dataTable.Rows.Add(dataRow);
}
return dataTable;
}
The first thing I do is to get the first element in the IEnumerable to perform the reflection on. Since IEnumerables don't expose an indexer, the only way I could think to do this is by performing a foreach loop and exiting as soon as I have a copy of the first element. I then get a list of the properties exposed by the object and create matching DataColumns for the DataTable. The second foreach loop populates a DataRow from each object and appends it to the table.
It's highly probable that I'll make other controls that iterate over IEnumerable data sources in this manner further down the road, so I opted to extract this functionality into an abstract base class called DataControl. This control exposes a public DataSource property for setting the IEnumerable, which converts it into a DataTable available to any derived classes through a protected property:
public abstract class DataControl : ISimpleControl
{
protected DataTable DataTable { get; set; }
public IEnumerable DataSource
{
set
{
DataTable = GetDataTableFromIEnumerable(value);
}
}
...
}
A derived class could then be used with an IEnumerable from your ViewData like so:
<%=Html.RenderControl(new MyDataControl{ DataSource = (IEnumerable)ViewData["MyLinqResult"] });%>
All that's left to do is to inherit from that DataControl and override the Render method to render the html:
public override string Render(HtmlHelper helper)
{
if (DataTable == null) return "";
// Use either the columns requested, or all table columns if not specified
string[] keys = DisplayColumns ?? GetColumnKeys(DataTable);
StringBuilder header = new StringBuilder();
// Build the html for the table's header
header.AppendLine(" <tr>");
foreach (string key in keys)
{
header.AppendLine(" <th>" + key + "</th>");
}
header.AppendLine(" </tr>");
bool altRow = true;
StringBuilder data = new StringBuilder();
// Render the relevant Html for each Row in the table
foreach (DataRow row in DataTable.Rows)
{
// Add an 'Alt' Css class to alternating rows
string rowClass = altRow ? "Alt" : "";
data.AppendLine(" <tr class=\"" + rowClass + "\">");
// Render the Html for each column of this row
foreach (string key in keys)
{
data.AppendLine(" <td>" + row[key] + "</td>");
}
data.AppendLine(" </tr>");
// Switch whether or not this is an alternating row
altRow = altRow ? false : true;
}
// Piece together all of the Html and return it
StringBuilder html = new StringBuilder();
html.AppendLine("<table>");
html.Append(header);
html.Append(data);
html.AppendLine("</table>");
return html.ToString();
}
I've also added a DisplayColumns property of type string[] to the Table class that enables me to specify the columns to render, as I might not want to render all of the properties. In line 6 I check for the existence of this property and if it hasn't been set I call GetColumnKeys() which is a private method I've written that returns a string[] of the DataColumn names from the DataTable.
I then render the html for the table header, and follow that up by rendering each row from the DataTable. I add an "Alt" class to each alternating row so I can use Css to colour the alternating rows differently.
I can now render a table in my View with a single line of code:
<%=Html.RenderControl(new Table{ DisplayColumns = new string[] { "Name", "EMail" },
DataSource = (IEnumerable)ViewData["MyLinqResult"] });%>
I'll finish this control in the next article by adding the functionality to click a row to fire an action, highlight a selected row, and include further actions in additional columns at the end of the table.
You can download the full source code from this article here: MvcTableControl.zip
Posted in Development on Friday, Friday, June 13, 2008 by Anthony Burns
My memory sucks. I'm like a knackered old 286 with 32MB of memory; it's all stored on the hard drive somewhere, but getting it into RAM and then to the CPU is a bit of a challenge. So after discovering memory tricks in Derren Brown's book Tricks of the Mind, I set about learning as much as I could on the subject and began putting it all to work.
The premise behind most memory techniques is that the average brain remembers images far better than it remembers facts or figures; so if you can build a picture in your head - the crazier the better (the picture not the head) - you're more likely to remember it than trying to remember numbers, words or dates and times alone.
It's all fairly straight forward; you want to memorise a shopping list of milk, bread and chicken: think of a big cartoon chicken swimming around in a lake full of milk while you throw loafs of bread at it like it's some sort of funfair game. It's strange, it's vivid and it's likely to stay in your head - at least until you get to the shop.
The problem comes when you need to memorise numbers, as is often the case in this technology driven world - pin numbers, phone numbers, registration numbers, dates, etc. The trick is to encode the numbers into letters; there are various techniques you can use, but the one that I took a shine to is called the Mnemonic Major System. There are a couple of variations, but the Derren Brown system dictates:
As an example, let's imagine that your PIN number is 9283. Break it up and start with the number 92; this encodes as GN - pad this out with some vowels of your own choosing and you can end up with GUN, GIN, GOON, GENE, you get the idea?
I then choose to encode the final two digits (83) as JAM. Now I build a big, colourful mental image of a gun that squirts out jam. You can also add something to the picture to remember the context, so let's imagine you're standing in your local bank, sticking the place up (no pun intended) with this gun which goes off randomly, covering the bewildered bank clerks with jam. When you need to, you can break this picture down to the words, then unencode the words back into the numbers.
The part I find tricky is packing the vowels in to find a decent word I can picture. This is where the Powershell and regular expressions come into play. I figured that given the right regular expression and a big list of English words, I could have any matching words recommended to me. For example, given the letters GN I could use the following regular expression to match any words that fit:
^[aeiou]*G[aeiou]*N[aeiou]*$
It basically matches any word on a line of its own that contains zero or more vowels, followed by a G then zero or more vowels, then an N, then anymore vowels that might be lying around.
All we need now is a function that takes the letters you've encoded, builds the regular expression and then finds the matches from a text file full of words. I was about to take the easy route and build something in C#, but I figured this would be a good opportunity to get my head around Powershell.
$letters = "gn"
$vowel = "[aeiou]"
$regex = "^" + $vowel + "*"
foreach($char in [char[]]$letters) { $regex += $char + $vowel + "*" }
$regex += "$"
I declare my vowels in a regular expression character class - you can actually use any letters that don't match up to a number here, so adding in things like W and Y will likely increase the amount of words returned. Then using a foreach construct I wrap each of the letters provided in my character class.
At this point we have our regular expression pattern, all we need is a list of words and some code to find the matches. After a quick google I found a list of words designed to help people play scrabble: WordList.txt. We can now use the Powershell command Get-Content (or its shorter alias gc) to load the words, then iterate though them looking for a match with our regular expression:
gc "WordList.txt" | foreach { if([regex]::match($_, $regex).Success) { $_ } }
We pipe the contents of the dictionary into the foreach loop, which makes each word available in the $_ variable. If we get a successful match from the [regex] then we write the word out to the Powershell console with the simple statement: $_
Wrapping the whole thing up in a function, we have our finished article:
function GetWords([string]$letters)
{
$vowel = "[aeiouy]"
$regex = "^" + $vowel + "*"
foreach($char in [char[]]$letters) { $regex += $char + $vowel + "+" }
$regex = $regex.substring(0, $regex.length - 1)
$regex += "*$"
gc "WordList.txt" | foreach { if([regex]::match($_, $regex).Success) { $_ } }
}
Paste this into a Powershell console window and then call it with: GetWords("gn") and you'll receive the following recommendations:
again, agene, agin, agon, agone, agony, eugenia, gaen, gain,
gan, gane, gaun, gen, gene, genie, genii, genoa, genu, genua,
gien, gin, gone, gonia, goon, gooney, goonie, goony, guan,
guanay, guano, guinea, gun, iguana, oogeny, oogonia, yogin,
yogini
If you want to learn more about memory tricks I'd highly recommend Derren Brown's book Tricks of the Mind, and anything by Dominic O'Brien.
Tagged as: memory, powershell, regex
Posted in Development on Wednesday, Wednesday, May 21, 2008 by Anthony Burns
DISCLAIMER: I'm in the privileged position of being able to dictate which browser our users use to access our extranet at work - I insist that IE must be used, not because I think it's a better browser, but because everyone has it installed by default and they know how to use it. By restricting to only one browser, I can speed up development times by not having to test our pages across multiple browsers. Because of that, this article is for IE only, and hasn't even been tested outside of IE7. If it proves interesting/helpful to people, then I might do an update later that works cross-browser.
I developed a webform for a project at work recently, that had a lot of form elements and that needed a file upload component adding.
The form was already too busy from a UI perspective and there wasn't a great location to plop down a file input element and submit button to upload the file. I decided I'd prefer it if I could just have an "Upload File" link, that when clicked prompted the user to select a file and caused the form to submit. I knew this was possible because Hotmail use a similar concept for attaching files to an email.
After a quick Google I discovered that the file input element in IE had a Javascript click() method - this I could use to open a file select dialog and I could then use form.submit() to upload the file. Simple.
I knocked up a quick prototype html file with the following code:
1: <script type="text/javascript">
2:
3: function UploadFile()
4: {
5: var form = document.UploadForm;
6: form.FileUpload.click();
7: form.SubmitButton.click();
8: }
9:
10: </script>
11:
12: <form id="UploadForm" name="UploadForm">
13: <input type="file" id="FileUpload" name="FileUpload" />
14: <input type="submit" value="Upload" id="SubmitButton" name="SubmitButton" />
15: </form>
When the link was clicked IE prompted me to select a file, but after I'd selected a file, IE gave me a Javascript "Access is denied" error. Bollocks.
After a long time spent on Google, I discovered that IE doesn't allow you to programmatically click the file input control and submit the form. Why on earth you'd want to click the control but not submit the form is beyond me. Even more bizarre is the fact that IE WILL allow you to do this, if the form is contained inside an IFrame. Bizarre, but a solution nonetheless.
So I constructed an UploadFile.aspx file containing the code below.
1: <form id="UploadForm" runat="server" enctype="multipart/form-data">
2: <asp:FileUpload ID="FileUpload" runat="server" />
3: <asp:HiddenField ID="UploadPath" runat="server" />
4: <asp:HiddenField ID="UploadLinkId" runat="server" />
5: <asp:Button UseSubmitBehavior="true" ID="SubmitButton" runat="server" />
6: </form>
7:
8: <a href="javascript:document.UploadForm.FileUpload.click();document.UploadForm.submit();">Upload File</a>
The form contains a file upload element, a submit button and two hidden fields. I plan on using javascript to set these hidden fields, one to enable me to specify a subfolder to upload the file to, and one to keep a reference to the "Upload File" link.
I then created a TestForm.aspx file containing a link and a hidden IFrame to contain my UploadFile.asp page:
1: <head>
2: <title>One-Click Upload</title>
3: <script type="text/javascript" src="Javascript/Upload.js"></script>
4: </head>
5: <body>
6:
7: <p><a href="#" id="UploadLink" onclick="return UploadFile(this, '');">Upload File</a></p>
8:
9: <iframe src="UploadFile.aspx" frameborder="0" id="UploadFrame" name="UploadFrame" style="display: none">
10: </iframe>
11:
12: </body>
13: </html>
In order to tie the "Upload File" link and the upload form within the IFrame together, I wrote the following Javascript function:
1: function UploadFile(link, uploadPath) {
2:
3: var uploadStatus = '<b>Uploading, please wait...</b>';
4:
5: // Check to see if we are already uploading a file
6: if(link.innerHTML.toLowerCase() == uploadStatus.toLowerCase()) {
7: alert('A file is currently being uploaded, please wait for it to finish before uploading another.');
8: } else {
9:
10: // Set our hidden fields and cause IE to prompt for a file
11: var form = UploadFrame.document.UploadForm;
12: form.UploadPath.value = uploadPath;
13: form.UploadLinkId.value = link.id;
14: form.FileUpload.click();
15:
16: // If the user has selected a file then submit the form
17: if(form.FileUpload.value != '') {
18: form.SubmitButton.click();
19:
20: // Make a copy of the link text and change it to show that the file is being uploaded
21: link.rel = link.innerHTML;
22: link.innerHTML = uploadStatus;
23: }
24: }
25:
26: return false;
27: }
It takes a reference to the link, and a subfolder to upload the file to as parameters. When called, it sets the ID of the link (to be utilised later on) and the upload path in the hidden form and then causes IE to prompt for a file. If a file is selected, it then submits the form and changes the text of the link to indicate that the upload is in progress.
On a subsequent click, it detects that the link contains the new text and explains to the user that an upload is already in progress.
So clicking the "Upload File" link prompts the user to select a file and if a file is selected, it submits the form upload the file.
The code behind the UploadFile.aspx file handles the uploading of the file:
1: protected void Page_Load(object sender, EventArgs e)
2: {
3: if (IsPostBack)
4: {
5: string filePath = ConfigurationManager.AppSettings["OneClickUploadFilePath"];
6: if (UploadPath.Value != "")
7: {
8: filePath = Path.Combine(filePath, UploadPath.Value);
9: }
10:
11: if (!Directory.Exists(filePath))
12: {
13: Directory.CreateDirectory(filePath);
14: }
15:
16: FileUpload.SaveAs(Path.Combine(filePath, FileUpload.FileName));
17:
18: Body.Attributes["onload"] = "UploadFinished('" + UploadLinkId.Value + "');";
19: }
20: }
First we get the upload path from the Application Settings of the web.config file (obviously you set this yourself in your own web.config. This is then combined with any subfolder passed through the form, the directory is created if it doesn't already exist and the uploaded file is saved to it.
The final line sets the onload attribute to call a Javascript function called UploadFinished with the ID of the link received from the form. That function can be used to notify the user that the file has finished uploading. In my case I also wanted to offer the option of refreshing the page, as my pages contain a list of the uploaded files:
1: function UploadFinished(linkId)
2: {
3: if(confirm('The file has finished uploading, would you like to refresh the page?')) {
4: parent.location.reload(true);
5: } else {
6: var link = parent.document.getElementById(linkId);
7: if(link != null) {
8: link.innerHTML = link.rel;
9: }
10: }
11: }
First it asks the user if they would like to refresh the page and if they click "Yes" then the page reloads. If they click "No", then the function uses the link ID - that has been passed through the form - to set the link text back to its original text, thereby enabling the user to upload an additional file.
So there we have it: upload a file using a single link rather than a pair of ugly form controls.
Tagged as: asp.net, csharp, javascript