Thomas Bandt

Über mich | Kontakt | Archiv

[MonoTouch] Mehrere Dateien + zusätzliche Formulardaten hochladen

Dateien mittels C# aus einer beliebigen Anwendung heraus hoch- und herunterzuladen, ist dank System.Net.WebClient kein großes Problem. Die Klasse stellt eigentliche alle benötigten Funktionen für diese Aktionen bereit, inklusive einer jeweils asynchronen Implementierung. Alle, bis auf eine: es lassen sich beim Upload von Dateien keine zusätzlichen Informationen übertragen.

Den Anwendungsfall kennt man aus einem Web-Formular: man gibt Vornamen, Nachnamen und ggf. weitere Textdaten ein, und wählt dann eine Datei aus. Alles zusammen wird an den Server gesendet und ausgewertet.

Das gleiche Szenario ist aber auch dann gar nicht so abwägig, wenn man sich nicht in einer HTML-Seite befindet.

Um das Ganze mit .NET-Boardmitteln zu erreichen, muss man einiges zu Fuß erledigen. Der nachfolgende Code ist das Produkt einer Implementierung für MonoTouch, funktioniert mit kleinen Änderungen so aber auch in jeder anderen beliebigen Mono- oder .NET-Applikation.

Ich habe mich beim Naming an die Konventionen der WebClient-Klasse gehalten, und zusätzlich noch Events für den Fortschritt und die Fertigstellung integriert. Eine Fehlerbehandlung erfolgt nicht.

public class FileUploader
{
    const string ParametersTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";
    const string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n";

    const int blockSize = 4096;

    public delegate void UploadFileCompletedHandler(string serverResponse);
    public event UploadFileCompletedHandler UploadFileCompleted;

    public delegate void UploadProgressChangedHandler(long bytesSent, long totalBytesToSend);
    public event UploadProgressChangedHandler UploadProgressChanged;

    public void UploadFileAsync(string uploadUrl, IEnumerable uploadFiles, NameValueCollection formParamters)
    {
        BackgroundWorker worker = new BackgroundWorker();
        int taskID;

        worker.DoWork += delegate(object sender, DoWorkEventArgs arguments) 
        {
            var result = new FileUploadResult();
            result.TaskID = (int)arguments.Argument;
            result.ServerResponse = FileUpload(uploadUrl, uploadFiles, formParamters);

            arguments.Result = result;
        };

        worker.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs arguments) 
        {
            var result = (FileUploadResult)arguments.Result;

            if(UploadFileCompleted != null)
                UploadFileCompleted(result.ServerResponse);

            UIApplication.SharedApplication.EndBackgroundTask(result.TaskID);
        };

        taskID = UIApplication.SharedApplication.BeginBackgroundTask(() => {});

        worker.RunWorkerAsync(taskID);
    }

    public string FileUpload(string uploadUrl, IEnumerable uploadFiles, NameValueCollection formParamters)
    {
        List files = uploadFiles.ToList();

        Console.WriteLine("Upload startet");
        
        string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
        byte[] boundarybytes = Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");
        
        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uploadUrl);
        webRequest.ContentType = "multipart/form-data; boundary=" + boundary;
        webRequest.Method = "POST";
        webRequest.KeepAlive = true;

        using (Stream requestStream = webRequest.GetRequestStream())
        {
            foreach (string key in formParamters.Keys)
            {
                requestStream.Write(boundarybytes, 0, boundarybytes.Length);
                
                string item = string.Format(ParametersTemplate, key, formParamters[key]);
                byte[] itemContent = System.Text.Encoding.UTF8.GetBytes(item);
                
                requestStream.Write(itemContent, 0, itemContent.Length);
            }
            
            requestStream.Write(boundarybytes, 0, boundarybytes.Length);

            long bytesSent = 0;
            long totalBytesToSend = 0;

            files.ForEach(f => totalBytesToSend += new FileInfo(f.FilePath).Length);
                            
            long onePercent = (long)(totalBytesToSend * 0.01);
            onePercent -= onePercent % blockSize;

            foreach(UploadFile file in files)
            {

                string header = string.Format(headerTemplate, file.ParameterName, file.FilePath, file.ContentType);
                byte[] headerBytes = System.Text.Encoding.UTF8.GetBytes(header);
                
                requestStream.Write(headerBytes, 0, headerBytes.Length);

                using (var fileStream = new FileStream(file.FilePath, FileMode.Open, FileAccess.Read))
                {
                    byte[] buffer = new byte[blockSize];
                    int bytesRead = 0;

                    while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
                    {
                        requestStream.Write(buffer, 0, bytesRead);
                        bytesSent += blockSize;

                        if(bytesSent % onePercent == 0 && UploadProgressChanged != null)
                            UploadProgressChanged(bytesSent, totalBytesToSend);
                    }

                    if(UploadProgressChanged != null)
                        UploadProgressChanged(totalBytesToSend, totalBytesToSend);
                }

                requestStream.Write(boundarybytes, 0, boundarybytes.Length);
            }
        }
        
        using (WebResponse webResponse = webRequest.GetResponse())
        {
            webRequest = null;
            using(StreamReader responseReader = new StreamReader(webResponse.GetResponseStream()))
            {
                return responseReader.ReadToEnd();
            }
        }
    }
}

public class UploadFile
{
    public string FilePath { get; set; }
    public string ContentType { get; set; }
    public string ParameterName { get; set; }
}

public class FileUploadResult
{
    public int TaskID { get; set; }
    public string ServerResponse { get; set; }
}

public class ContentType
{
    public static string Default = "application/octet-stream";
    public static string Jpeg = "image/jpeg";
}

Über

long onePercent = (long)(totalBytesToSend * 0.01);
onePercent -= onePercent % blockSize;

wird das Intervall definiert, in welchem das Fortschritt-Event gefeuert werden soll. Hier nach einem Prozent der übertragenen Datenmenge. Hintergrund: würde man es nach jedem Block auslösen und würde in der Implementierung bspw. das GUI aktualisiert - etwa eine Progressbar, wie in meinem Fall - dann wäre der Upload plötzlich sehr, sehr langsam ;-).

Die Verwendung ist übrigens denkbar einfach und analog zum WebClient:

private void StartUpload()
{
    var uploader = new FileUploader();

    uploader.UploadFileCompleted += delegate(string serverResponse)
    {
        Console.WriteLine("Fertig. Ergebnis: " + serverResponse);
    };

    uploader.UploadProgressChanged += delegate(long bytesSent, long totalBytesToSend)
    {
        InvokeOnMainThread(delegate {    
            float progress = (float)bytesSent / (float)totalBytesToSend;
            
            _progressBar.Progress = progress;
            
            _progressInfo.Value = (int)(progress * 100) + "%";
            ReloadData();
        });
    };

    var parameters = new NameValueCollection();
    parameters.Add("FirstName", "Max");
    parameters.Add("LastName", "Muster");

    uploader.UploadFileAsync(
        "http://192.168.xxx.xx/",
        new [] {
            new UploadFile { 
                FilePath = Path.Combine(NSBundle.MainBundle.BundlePath, "Canvas.psd"),
                ParameterName = "file1",
                 ContentType = ContentType.Default
            },
            new UploadFile { 
                FilePath = Path.Combine(NSBundle.MainBundle.BundlePath, "Picture.jpg"),
                ParameterName = "file2",
                ContentType = ContentType.Jpeg
            }
        },
        parameters
    );
}
In einer ASP.NET-MVC-Anwendung ließe es sich wie folgt auslesen:
public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

[HttpPost]
public ActionResult MetaUpload(User user, HttpPostedFileBase file1, HttpPostedFileBase file2)
{
    file1.SaveAs(Path.Combine(@"C:\Temp\", Path.GetFileName(file1.FileName)));
    file2.SaveAs(Path.Combine(@"C:\Temp\", Path.GetFileName(file2.FileName)));
    return Json("Angekommen: " + user.FirstName + " " + user.LastName);
}

Viel Spaß damit.

(Die Inspiration und große Teile des Codes stammen von hier.)



« Zurück  |  Weiter »