Click here to Skip to main content
15,896,063 members
Articles / Web Development / HTML
Article

Setting a file to upload inside the WebBrowser component

Rate me:
Please Sign up or sign in to vote.
3.31/5 (9 votes)
28 Aug 2008CPOL3 min read 105.4K   3.9K   25   19
Changing the value of file input fields in WebBrowser made easy.

Introduction

I needed to attach files silently to HTML forms hosted in a WebBrowser control without the user having to point to those files manually.

As you probably know, for security reasons, it is not possible to change the value of the HTML form file input programmatically. My first approach was to set focus on the input field and then do some SendKeys.SendWait() calls to set the value. The field can be hidden away from screen by setting the absolute position and negative coordinates for it. Although it had some drawbacks, it worked fine in a stand-alone app, but unfortunately, inside a Word add-in, file inputs cannot receive keyboard input for some reason.

After examining the methods of the WebBrowser, I found out that I can send arbitrary POST data using the Navigate() method:

C#
WebBrowser.Navigate(Uri url, string targetFrameName, 
                    byte[] postData, string additionalHeaders)

There is a good example of how to make correct multipart request headers and body here, so I decided to use it. (You may also refer to RFC 1867.)

The solution: a helper class

To simplify the process of making requests, I wrote the helper class (included in the archive), so adding a file to the form can be as simple as:

C#
private void webBrowser1_DocumentCompleted(object sender, 
             WebBrowserDocumentCompletedEventArgs e)
{
    HtmlElement form = webBrowser1.Document.Forms[0];
    form.AttachEventHandler("onsubmit", delegate(object o, EventArgs arg)
        {
            FormToMultipartPostData postData = 
                new FormToMultipartPostData(webBrowser1, form);
            postData.SetFile("fileField", @"C:\windows\win.ini");
            postData.Submit();
        });
}

In the example above, I’m adding an event handler for the “onsubmit” event, which intercepts the submit process, prepares new multipart POST messages with the actual data in the form, attaches a file to the input field with a specific name (“fileField”), and then sends the result to the server (interrupting the original submit process).

Here’s a more complicated (and more correct) example, which manages several forms on one page (adding onsubmit handler only for forms with a “multipart/form-data” enctype attribute):

C#
private void webBrowser1_DocumentCompleted(object sender, 
             WebBrowserDocumentCompletedEventArgs e)
{
    HtmlDocument doc = webBrowser1.Document;
    for (int i = 0; i < doc.Forms.Count; i++)
    {
        // must be declared inside the loop because there's a closure
        HtmlElement form = doc.Forms[i];
        if (form.GetAttribute("enctype").ToLower() != "multipart/form-data") { continue; }
        
        form.AttachEventHandler("onsubmit", delegate(object o, EventArgs arg)
        {
            FormToMultipartPostData postData = 
                new FormToMultipartPostData(webBrowser1, form);
            postData.SetFile("file", @"C:\windows\win.ini");
            postData.Submit();
        });
        form.SetAttribute("hasBrowserHandler", "1");
        // expose that we have a handler to JS
    }
}

The FormToMultipartPostData class (not the best name, I know), which does all the ‘dirty’ work, must be instantiated just before submitting a form.

From the constructor, it loads all the values set in the form (GetValuesFromForm() method). The central method is GetEncodedPostData(), which returns all the necessary parameters to make a POST request with WebBrowser.Navigate() (header string and request body as a byte array). Submit() is another convenience method to invoke WebBrowser.Navigate() with the request data.

Because the class is rather large (compared to its modest functionality), I decided not to post it here. You can find it in the archive.

Handling JavaScript: form.submit()

By design, calling the submit() method on a form in JavaScript does not fire the onsubmit event, i.e., its handler will not be executed. To provide a work-around, I decided to set a handler for WebBrowser.Navigating, which is invoked before the page location is changed:

C#
private void webBrowser1_Navigating(object sender, WebBrowserNavigatingEventArgs e)
{
    string url = e.Url.ToString();
    if (url.StartsWith("submit:"))
    {
         string formId = url.Substring(7);
         HtmlElement form = webBrowser1.Document.GetElementById(formId);
         if (form != null) { form.RaiseEvent("onsubmit"); }
         e.Cancel = true;
    }
}

The trick is to set the window location to fake the URL which starts with “submit:” (the rest of this “pseudo-URL” is the form ID). If anyone knows a more elegant solution, please share it with me.

You may have noted in the above “complicated example” of adding an onsubmit handler that there is an additional attribute to expose the presence of the handler to the JavaScript:

C#
form.SetAttribute("hasBrowserHandler", "1");

Thus, the JavaScript code needed to invoke the submit process should be something like this:

JavaScript
function doSubmit(formId) {
  var form = document.getElementById(formId);
  if (form.getAttribute('hasBrowserHandler')) {
    window.location = "submit:" + formId;
  }
  else {
    form.submit();
  }
}

The test script

I also included a PHP script which might be useful in testing and debugging the class.

Credits

Thanks to Steven Cheng from Microsoft, who posted an example of making multipart messages, I did not have to write the main (and most difficult) part of the code. There’s also an example of the same task done using the HttpWebRequest class on CodeProject.

I hope this solution will help you save some time (I spend a whole day to find the best way to solve this simple task.) Since at this time I am a newbie to C#, any feedback is appreciated.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWhat if the user uses other programs that it does not affect Pin
cheddad201828-Feb-20 16:28
cheddad201828-Feb-20 16:28 
QuestionHow we can upload image automatically using webbrowser on ebayclassifieds.com and backpage.com? Pin
ashishgupta12121-Aug-13 2:13
ashishgupta12121-Aug-13 2:13 
SuggestionThis code without the JavaScript Pin
r-ISP20-Apr-13 11:20
professionalr-ISP20-Apr-13 11:20 
SuggestionRe: This code without the JavaScript Pin
r-ISP21-Apr-13 2:50
professionalr-ISP21-Apr-13 2:50 
QuestionThis Post is very helpul for me Pin
ashishgupta12129-Feb-13 13:35
ashishgupta12129-Feb-13 13:35 
AnswerRe: This Post is very helpul for me Pin
r-ISP26-Apr-13 8:04
professionalr-ISP26-Apr-13 8:04 
GeneralMy vote of 2 Pin
tyk3731-Aug-12 5:02
tyk3731-Aug-12 5:02 
QuestionpostData.setfile Pin
tyk3730-Aug-12 12:07
tyk3730-Aug-12 12:07 
QuestionAs if I want to use more than one file? Pin
lcsistemas19-Jul-12 21:14
lcsistemas19-Jul-12 21:14 
QuestionConvert to vb.net Pin
NanoParedaz28-Jun-12 19:33
NanoParedaz28-Jun-12 19:33 
SuggestionMissing bytes on the encoded data? Pin
olivax1-Mar-12 6:46
olivax1-Mar-12 6:46 
GeneralRe: Missing bytes on the encoded data? Pin
Fernando JB18-Apr-13 9:15
Fernando JB18-Apr-13 9:15 
QuestionQuestion - webBrowser1_Navigating method Pin
donaldn200621-Feb-11 7:37
donaldn200621-Feb-11 7:37 
Questiontrying to get it to work - 3 questions Pin
donaldn200621-Feb-11 7:02
donaldn200621-Feb-11 7:02 
GeneralVB.NET code problem Pin
Member 450769726-May-10 7:46
Member 450769726-May-10 7:46 
i convert your c# code into vb.net code.
then i can get fileupload.postedfile in ASP.NET page but filename is blank and contentlength is 0.
if i execute c# code then in ASP.NET page i get filename as well as content legth.


here i past VB.NET code of your class-FormToMultipartPostData

Please study this class
is there any thing wrong?

Thanks in advance.
Please reply


Imports System.IO
Imports System.Collections.Generic

Public Class FormToMultipartPostData
Private Structure ValuePair
' KeyValuePair<string, string> sounds too verbose for me
Public name As String
Public value As String
Public Sub New(ByVal name As String, ByVal value As String)
Me.name = name
Me.value = value
End Sub
End Structure

Public Structure RequestParameters
Public data As Byte()
Public headers As String
End Structure

Private values As New List(Of ValuePair)()
Private files As New List(Of ValuePair)()
Private overloadedFiles As New Dictionary(Of String, String)()

Private form As HtmlElement
Private webbrowser As WebBrowser
'*
' * In most circumstances, this constructor is better (allows to use Submit() method)
'
Public Sub New(ByVal b As WebBrowser, ByVal f As HtmlElement)
form = f
webbrowser = b
GetValuesFromForm(f)
End Sub
'*
' * Use this constructor if you don't want to use Submit() method
'
Public Sub New(ByVal f As HtmlElement)
GetValuesFromForm(f)
End Sub
'*
' * Submit the form
'
Public Sub Submit()
Dim url As New Uri(webbrowser.Url, form.GetAttribute("action"))
Dim req As RequestParameters = GetEncodedPostData()
webbrowser.Navigate(url, form.GetAttribute("target"), req.data, req.headers)
End Sub
'*
' * Load values from form
'
Private Sub GetValuesFromForm(ByVal form As HtmlElement)
' Get values from the form
For Each child As HtmlElement In form.All
Select Case child.TagName
Case "INPUT"
Select Case child.GetAttribute("type").ToUpper()
Case "FILE"
AddFile(child.Name, child.GetAttribute("value"))
Exit Select
'Case "CHECKBOX", "RADIO"
' 'If child.GetAttribute("checked") = "True" Then
' 'AddValue(child.Name, child.GetAttribute("value"))
' 'End If
' 'Exit Select
'Case "BUTTON", "IMAGE", "RESET"
' Exit Select
Case Else
' Ignore those?
AddValue(child.Name, child.GetAttribute("value"))
Exit Select
End Select
Exit Select
Case "TEXTAREA", "SELECT"
' it's legal in IE to use .value with select (at least in IE versions 3 to 7)
AddValue(child.Name, child.GetAttribute("value"))
Exit Select
' of "switch tagName"
End Select
' of "foreach form child"
Next
End Sub

Private Sub AddValue(ByVal name As String, ByVal value As String)
If name = "" Then
Exit Sub
End If
' e.g. unnamed buttons
values.Add(New ValuePair(name, value))
End Sub

Private Sub AddFile(ByVal name As String, ByVal value As String)
If name = "" Then
Exit Sub
End If
files.Add(New ValuePair(name, value))
End Sub

'*
' * Set file field value [the reason why this class exist]
'

Public Sub SetFile(ByVal fieldName As String, ByVal filePath As String)
Me.overloadedFiles.Add(fieldName, filePath)
End Sub

'*
' * One may need it to know whether there's specific file input
' * For example, to perform some actions (think format conversion) before uploading
'

Public Function HasFileField(ByVal fieldName As String) As Boolean
For Each v As ValuePair In files
If v.name = fieldName Then
Return True
End If
Next
Return False
End Function

'*
' * Encode parameters
' * Based on the code by Steven Cheng, http://bytes.com/forum/thread268661.html
'

Public Function GetEncodedPostData() As RequestParameters
Dim boundary As String = "----------------------------" & DateTime.Now.Ticks.ToString("x")

Dim memStream As Stream = New System.IO.MemoryStream()
Dim boundarybytes As Byte() = System.Text.Encoding.ASCII.GetBytes(vbCr & vbLf & "--" & boundary & vbCr & vbLf)

Dim formdataTemplate As String = vbCr & vbLf & "--" & boundary & vbCr & vbLf & "Content-Disposition: form-data; name=""{0}"";" & vbCr & vbLf & vbCr & vbLf & "{1}"
For Each v As ValuePair In values
Dim formitem As String = String.Format(formdataTemplate, v.name, v.value)
Dim formitembytes As Byte() = System.Text.Encoding.UTF8.GetBytes(formitem)
memStream.Write(formitembytes, 0, formitembytes.Length)
Next
memStream.Write(boundarybytes, 0, boundarybytes.Length)

Dim headerTemplate As String = "Content-Disposition: form-data; name=""{0}""; filename=""{1}""" & vbCr & vbLf & "Content-Type: application/octet-stream" & vbCr & vbLf & vbCr & vbLf

For Each v As ValuePair In files
Dim filePath As String

If overloadedFiles.ContainsKey(v.name) Then
filePath = overloadedFiles(v.name)
Else
If v.value.Length = 0 Then
Continue For
End If
' no file
filePath = v.value
End If

Try
' file can be absent or not readable
Dim fileStream As New FileStream(filePath, FileMode.Open, FileAccess.Read)

Dim header As String = String.Format(headerTemplate, v.name, filePath)
Dim headerbytes As Byte() = System.Text.Encoding.UTF8.GetBytes(header)
memStream.Write(headerbytes, 0, headerbytes.Length)

Dim buffer As Byte() = New Byte(1023) {}
Dim bytesRead As Integer = 0
While (InlineAssignHelper(bytesRead, fileStream.Read(buffer, 0, buffer.Length))) <> 0
memStream.Write(buffer, 0, bytesRead)
End While

memStream.Write(boundarybytes, 0, boundarybytes.Length)
fileStream.Close()
Catch x As Exception
' no file?..
MessageBox.Show(x.Message, "Cannot upload the file", MessageBoxButtons.OK, MessageBoxIcon.Warning)
End Try
Next

Dim result As New RequestParameters()

memStream.Position = 0
result.data = New Byte(memStream.Length - 1) {}
memStream.Read(result.data, 0, result.data.Length)
memStream.Close()

result.headers = ("Content-Type: multipart/form-data; boundary=" & boundary & vbCr & vbLf & "Content-Length: ") + result.data.Length & vbCr & vbLf & vbCr & vbLf

Return result
End Function
Private Shared Function InlineAssignHelper(Of T)(ByRef target As T, ByVal value As T) As T
target = value
Return value
End Function
End Class
GeneralIt's work Pin
Wojciech Nagórski1-Oct-09 6:40
Wojciech Nagórski1-Oct-09 6:40 
Generalnot working... Pin
djdane14-May-09 4:45
djdane14-May-09 4:45 
QuestionHi Kirill Hryapin ( Code Project User ) Pin
kjhaveri26-Nov-08 19:49
kjhaveri26-Nov-08 19:49 
GeneralCouldnt get it to work with particular URL Pin
Conker5-Oct-08 23:30
Conker5-Oct-08 23:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.