Upload und Speichern von Dateien in einer DB mit einer C# ASP.NET MVC Applikation

uploadMVC mit C# macht ja grundsätzlich Spass (obwohl für einige Leute ja auch Auspeitschen angenehm ist :)). Ich habe ein paar spezielle Aufgaben zum Realisieren gefasst und gebe hier ein paar Weisheiten von mir, die wahrscheinlich allgemein bekannt sind und ich einfach nur nicht finden kann.

In diesem Beitrag wird beschrieben, wie man verschiedene Medien (Bilder, Filme, Audio) hochladen, in einer Datenbank speichern und (mit HTML 5) wieder darstellen kann. Ich gehe aus von einer frisch erstellten und herausgeputzten MVC 3 Webapplikation.

Am Schluss sollte die Liste in etwa so aussehen:

Liste der Medien

M wie Model

Mein Model speichert einen Namen, den Content- (oder MIME-) Type und die binären Daten des Mediums. Aus dem folgenden Model macht das Entity-Framework ein erstaunlich gutes SQL-Schema:

// Sie ist ein Model und sie sieht gut aus...
namespace MediaPOC.Models {
    public class Media {
       
        public virtual int MediaId { get; set; }
        [Required]
        [StringLength(150)]
        public virtual string Name { get; set; }
        [Required]
        [StringLength(250)]
        [ScaffoldColumn(false)]
        public virtual string MimeType { get; set; }
        [Required]
        public virtual byte[] Data { get; set; }
    }
}

Daraus kann man nun den Rest mal scaffolden. Das Produkt wird aber noch nicht ganz funktionieren, da sich MVC 3 keinen Reim auf das „Data“ Feld machen kann.

V wie View

Der grundsätzliche Trick besteht darin, dem file-upload-Tag einen anderen Namen zu geben um so die Datei ganz kontrolliert im Model abspeichern zu können. Ebenfalls muss der Form-Tag für den Upload angepasst werden. Die wichtigen Teile von Create.cshtml (und ev. auch Edit.cshtml):

@using (Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" })) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Media</legend>
        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Data)
        </div>
        <div class="editor-field">
            <input type="file" name="file" />
            @Html.ValidationMessageFor(model => model.Data)
        </div>
        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

C wie Controller

Die Datei wird dem Controller als zusätzliches Argument übergeben. Ein Grundproblem ist, dass das Model wegen dem leeren Date ungültig ist. Ein Revalidate nach dem Speichern der Daten besänftigen die MS-Götter:

        //
        // POST: /Home/Create

        [HttpPost]
        public ActionResult Create(Media media, HttpPostedFileBase file)
        {
            // Do we have a file
            if (file != null && file.ContentLength > 0)
            { // Yes
                media.MimeType = file.ContentType;
                media.Data = new byte[file.ContentLength];
                file.InputStream.Read(media.Data, 0, file.ContentLength);
            }
            // If a file was supplied, we have saved the data in teh model
            ModelState.Clear();
            TryValidateModel(media);
            if (ModelState.IsValid)
            {
                db.Media.Add(media);
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            return View(media);
        }

Und das wars schon.

Moment, wie kriegen wir die Daten wieder raus?

Nun, äääähm, ja. Ganz grundsätzlich braucht es sicher eine Möglichkeit, die Daten dem Browser zu senden. Die folgende Lösung ist einfach und nett, hat allerdings ein Problem: Der Dateiname bei einem Download ist hässlich.

namespace MediaPOC.Controllers {
    public class MediaController : Controller {
        private MediaPOCContext db = new MediaPOCContext();

        //
        // GET: /Media/Show/5
        public ActionResult Show(int id) {
            Media media = db.Media.Find(id);
            return File(media.Data, media.MimeType);
        }
    }
}

Um in den Views locker zu bleiben habe ich mir HTML5 zu Hilfe geholt. Ein View-Helper soll jeweils einen korrekten Media-Tag und ein Link darstellen. Bei Audio/Video wird das Element unsichtbar gemacht, bis man sich sicher ist, dass der Browser das Format beherrscht. Das Format des Mediums wird beim Tag in einem data-type Attribut gespeichert. Angewendet wird der Tag in einer View mittels:

@Html.MediaTag(mediaObject)

Ich weiss, der Code ist leicht redundant, aber ich wollte Audio und Video aus irgendeinem ganz wichtigen Grund, den ich vergessen habe, trennen:

namespace MediaPOC.Helpers {
    public static class HtmlHelpers {
        public static MvcHtmlString MediaTag(this HtmlHelper helper, Media media, int thumbnailsize=50) {
            UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
            string url = urlHelper.Action("Show", "Media", new { id = media.MediaId });

            TagBuilder mediaTag;
            TagBuilder brTag= new TagBuilder("br");

            string tag="";

            if(media.MimeType=="image/png" || media.MimeType=="image/gif" || media.MimeType== "image/jpeg") {
                    TagBuilder imageTag = new TagBuilder("img");
                    imageTag.MergeAttribute("src", url);
                    imageTag.MergeAttribute("class", "thumbnail");

                    mediaTag = new TagBuilder("a");
                    mediaTag.MergeAttribute("href", url);
                    mediaTag.InnerHtml = imageTag.ToString();
                    tag=mediaTag.ToString(TagRenderMode.Normal);
                   
            }
            else if (media.MimeType.StartsWith("audio/"))
            {

                TagBuilder aTag = new TagBuilder("a");
                aTag.MergeAttribute("href", url);
                aTag.InnerHtml = media.Name;


                mediaTag = new TagBuilder("audio");
                mediaTag.MergeAttribute("data-type", media.MimeType);
                mediaTag.MergeAttribute("style", "display:none");
                mediaTag.MergeAttribute("class", "thumbnail");
                mediaTag.MergeAttribute("src", url);
                mediaTag.MergeAttribute("controls", "controls");
                mediaTag.InnerHtml = aTag.ToString();
                tag = mediaTag.ToString(TagRenderMode.Normal) + brTag.ToString()+aTag.ToString();

            }
            else if (media.MimeType.StartsWith("video/"))
            {

                TagBuilder aTag = new TagBuilder("a");
                aTag.MergeAttribute("href", url);
                aTag.InnerHtml = media.Name;


                mediaTag = new TagBuilder("video");
                mediaTag.MergeAttribute("data-type", media.MimeType);
                mediaTag.MergeAttribute("style", "display:none");
                mediaTag.MergeAttribute("class", "thumbnail");

                mediaTag.MergeAttribute("src", url);
                mediaTag.MergeAttribute("controls", "controls");
                mediaTag.InnerHtml = aTag.ToString();
                tag = mediaTag.ToString(TagRenderMode.Normal) + brTag.ToString() + aTag.ToString();

            }
            else
            {
                mediaTag = new TagBuilder("a");
                mediaTag.SetInnerText(media.Name);
                mediaTag.MergeAttribute("href", url);
                tag = mediaTag.ToString(TagRenderMode.Normal);
            }
           
            return new MvcHtmlString(tag);
        }
    }//end of class
}//end of namespace

Um die entsprechenden Elemente sichtbar zu machen, wenn der Browser sie beherrscht, muss man noch etwas JavaScript absondern:

$(function () {
    $("audio").each(function (index) {

        var audioElement = document.createElement('audio');
        var canPlayType = audioElement.canPlayType($(this).attr('data-type'));

        if (canPlayType.match(/maybe|probably/i)) {
            $(this).show();
        }

    });
    $("video").each(function (index) {

        var videoElement = document.createElement('video');
        var canPlayType = videoElement.canPlayType($(this).attr('data-type'));

        if (canPlayType.match(/maybe|probably/i)) {
            $(this).show();
        }

    });
});

So, das wars. Super.

8 Gedanken zu “Upload und Speichern von Dateien in einer DB mit einer C# ASP.NET MVC Applikation

  1. // Sie ist ein Model und sie sieht gut aus…
    Sie trinkt in der Bar immer Sekt… KORREKT! =D

    Toller Artikel hab ich gleich mal ausprobiert. Ausserdem hab‘ ich an Stelle von Metadaten in der Datenbank die Tags in C# modifiziert und spiele mit dem Gedanken das Endprodukt an all die Plattenriesen zu verkaufen… 😉

  2. Hi,

    habe das blöde Gefühl dass das „Edit“ deiner Application das vorhandene Bild nicht anzeigen wird… Oder liege ich da falsch?

  3. Das Gefühl ist grad gar nicht blöd, aber das obige Codesnipped zeigt ja nur die View für „Create“. Wenn Du die Mediendatei im Edit haben möchtest, musst Du in Edit.cshtml irgendwo

    @Html.MediaTag(Model)

    einfügen.

  4. Ganz ausreichend ist das dann aber noch immer nicht ;).

    Ich bin auf der Suche nach genau so einer Lösung, stolpere aber andauern über alles mögliche an gescripteten Kram.

    Alles top beschrieben und echt einfach verständlich, aber Validation und Edit quasi „aussen vor“ gelassen. Leider.

  5. Ich schreibs jetzt einfach unter den aktuellsten Blogeintrag, auch wenn’s nichts mit dem Thema zu tun hat:
    Klasse Blog! Gefällt mir wirklich SEHR gut, also ein dickes Lob an dieser Stelle.

    Randfrage: Ich bin eigentlich kein Blogleser und suche seit geraumer Zeit nach einem Feedcatcher/Feedreader, eben nach einem geeigneten Programm um Blogs zu lesen (und evtl. auch Podcasts zu hören).
    Gibt’s da ne Empfehlung?! 🙂

    Herzliche Grüße von einem Informatik-Lehramtstudenten.

  6. Vielen herzlichen Dank für das Lob. Leider wird dieser Blog im Moment sehr vernachlässigt weil ich an einem grösseren Projekt arbeite, aber ich hoffe, das bessert wieder.

    An die Frage nach dem Blog-Leser würde ich mich gerne anschliessen. Ich habe echt noch nichts vernünftiges gefunden. Am Besten wäre einer, der auch auf Tablets synchronisiert und das Offline-Lesen erlaubt…

    Viel Spass und Erfolg beim Studium!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.