Vorschau bei Bildupload

Ich finde es ein nettes Feature, eine kleine Vorschau anzuzeigen, wenn ich ein Bild auf einer Webseite hochladen will.
Dazu folgendes kleines Snippet:

$(function() {
    $("#fileUpload").change(function() {
        if (this.files && this.files[0]) {
            var file = this.files[0];

            var preview = $('#imagePreview');

            var isImage = typeof file.type !== "undefined" && file.name.match(/\.(png|jpe?g)$/i);

            if (isImage && typeof FileReader !== "undefined") {
                var reader = new FileReader();

                reader.onload = function(e) {
                    preview.empty();
                    var img = $('<img>');
                    img.attr('src', e.target.result);

                    if (preview.css('max-height') != 'none') {
                        img.css('max-height', parseInt(preview.css('max-height'), 10));
                    }

                    preview.html(img);
                }

                reader.readAsDataURL(this.files[0]);

                preview.show();
            } else {
                preview.empty();
                preview.html($('<p>').text('Dateiname: ' + file.name));
                preview.show();
            }
        }
    });
});

Das passende Formular dazu:

<div class="form-group">
    <label for="fileUpload" class="control-label col-md-2">Datei: </label>
    <div class="col-md-10">
        <input type="file" name="fileUpload" id="fileUpload"/>
    </div>
</div>
                    
<div class="form-group">
    <div class="col-md-10 col-md-offset-2 fileInputPreview" id="imagePreview">
        <!-- empty tag for preview image -->
    </div>
</div>

Die passenden CSS Klassen, damit das Bild auch nicht aus dem Design rauswandert 🙂

.fileInputPreview {
    display: inline-block;
    max-height: 200px;
    width: 500px;
}

.fileInputPreview img {
    max-width: 100%;
    height: auto;
}

Am Ende sieht es dann etwa so aus:
imagePreview

Den File-Upload habe ich mit Bootstrap FileStyle optisch etwas ansprechender gestaltet.

jQuery – Seitennavigation abbrechen

Fast jedes Programm weißt den Benutzer darauf hin, wenn es ungespeicherte Änderungen gibt, sobald man das Programm verlassen will.
Im Browser gibt es sowas standardmäßig nicht, aber dank JavaScript ist das kein Problem. Und mit jQuery wird die ganze Sache sogar noch einfacher.

Und zwar kann man sich an das „beforeUnload“-Event hängen. Hier wird der zu erscheinene Text hinterlegt. Weiteren Einfluss auf die Gestaltungsmöglichkeit hat man nicht, dies hängt vom Browser ab.

$("form").one("change", ":input", function() {
    // einen Hinweis anzeigen (wer es möchte)
    $('#saveInfo').show();

    // Bevor die Seite verlassen wird, wird der folgende Text angezeigt.
    $(window).on('beforeunload', function (e) {
            return "Es gibt ungespeicherte Änderungen.";
    });

    // Verhindern, dass ein Submit-Button (also z.B. "Speichern") auch den Hinweis hervorruft.
    $(this).parents("form:first").find(":submit").click(function() { $(window).off("beforeunload"); });
});

In dem Beispiel überwache ich alle Eingabefelder („:input„) innerhalb meiner Form. Sobald ein Feld geändert wird das „beforeunload“-Event scharf gemacht. Achtet auch auf die letzte Zeile, sollte ich über einen Submit-Button kommen („:submit„), wird das Event wieder aufgehoben. Schließlich will ich ja beim Klick auf „Speichern“ nicht den Hinweis bekommen.

C# – Die Falle mit dem this[]-Indexer und dem Enum

Vor ein paar Tagen kam ein Kollege auf mich zu und meinte, er habe beim Unit-Test schreiben einen Fehler in meinem Code entdeckt. Um mir den Fehler zu zeigen, hat er mir eigens einen Unit-Test geschrieben, welcher in etwa so aussah:

// MyTypeList : List<string>
MyTypeList list = new MyTypeList {"a", "b", "c"};

if(list.ElementAt(0) == null)
{
    throw new NullReferenceException();
}

if(list[0] == null)
{
    // Hier knallt er!
    throw new NullReferenceException();
}

if(list[1] == null)
{
    throw new NullReferenceException();
}

Er knallte immer, wenn man auf list[0] zugreifen wollte. Im Debugger zeigte er mir alle Elemente an, list[1] usw. funktionierte auch. Ich stand erstmal auf dem Schlau. Da ich den Fehler nicht sofort identifizieren konnte, baute ich mir eine kleine Testapplikation, um mit einem Minimum an Code das Problem zu identifizieren.

Letzendlich lag es an einem Indexer, welchen ich selber erstellt habe. Und zwar erwartete dieser als Übergabetyp ein Enum. Dieser überschreibt zwar nicht den von List<T> geerbten Indexer this[int index], jedoch scheint der Compiler hier leichte Probleme zu machen.

Schauen wir uns mal den kompilierten Code in dotPeek an.

      MyTypeList myTypeList1 = new MyTypeList();
      myTypeList1.Add("a");
      myTypeList1.Add("b");
      myTypeList1.Add("c");
      MyTypeList myTypeList2 = myTypeList1;
      if (Enumerable.ElementAt<string>((IEnumerable<string>) myTypeList2, 0) == null)
        throw new NullReferenceException();
      if (myTypeList2[MyEnum.Value1] == null)
        throw new NullReferenceException();

Den ersten Zugriff auf den numerischen Indexer wurde auf den Enum-Indexer geändert. Eine Erklärung dazu konnte ich nicht wirklich finden, bzw. keine die ich nachvollziehen konnte. Letztendlich hat es damit zu tun, dass ein Enum im Bauch eigentlich nur ein int-Typ ist. Dies passiert immer, wenn ich den numerischen Indexer mit 0 zugreife, wohlgemerkt, nur mit 0. Alle anderen Werte funktionieren problemlos.

Wie kann ich das Problem lösen / umgehen?

Dafür gibt es mehrere Wege.

Wird der Index-Wert woher in eine Variable (z.B. innerhalb einer for-Schleife) gesetzt, kann er den korrekten Indexer auflösen.

int index = 0;
if(list[index] == null) {
...
}

Ein Cast direkt auf int hingegen funktioniert nicht (er nutzt wieder den Enum-Indexer):

if(list[(int) 0] == null) {
..
}

Lustiger Weise lässt sich die Solution nicht mehr kompilieren, sobald ich einen weiteren Enum-Indexer hinzufüge:

The call is ambiguous between the following methods or properties: 'DumpSolution.MyTypeList.this[DumpSolution.MyEnum]' and 'DumpSolution.MyTypeList.this[DumpSolution.MyEnum2]'

Die simpelste Lösung ist, einfach den Standard-Indexer zu überschreiben:

    public class MyTypeList : List<string>
    {
        public string this[MyEnum myEnum]
        {
            get { return this.FirstOrDefault(item => item == Enum.GetName(myEnum.GetType(), myEnum)); }
        }

        public string this[MyEnum2 myEnum]
        {
            get { return this.FirstOrDefault(item => item == Enum.GetName(myEnum.GetType(), myEnum)); }
        }

        public new string this[int index]
        {
            get { return base[index]; }
            set { base[index] = value; }
        }
    }

Danach funktioniert der Code oben problemlos und korrekt. Ach ja, ein anpassen des Enums wie folgt brachte auch keinen Erfolg.

enum MyEnum {
   Value1 = 1,
   Value2 = 2
}

Ich kann mir vorstellen, dass es das Problem auch bei anderen Listen gibt, ich selbst habe es aber aktuell nur bei List<T> festgestellt. Im Netz findet man einige Einträge dazu, dazu auch eine Erklärung (welche mich aber nur bedingt zufrieden stellt)

Ich habe ein kleines Beispiel, welches ihr hier herunterladen könnt.

ASP.NET MVC & Bootstrap 3.0: Client Validierung

Wie man die Server-Validierung für Bootstrap 3 ein wenig aufhübschen kann, habe ich ja bereits erklärt.

Nun wollte ich auch schauen, dass die Client-Variante etwas _bootstrappiger_ aussieht und bin auf folgenden Beitrag gestoßen. Sehr gut finde ich, dass hier sämtliche Standardmethoden genutzt werden und das Rad nicht neu erfunden wird. Das ganze ist nur für Bootstrap 2, daher habe ich noch ein wenig dran geschraubt.

jQuery.validator.setDefaults({
    highlight: function (element, errorClass, validClass) {
        if (element.type === 'radio') {
            this.findByName(element.name).addClass(errorClass).removeClass(validClass);
        } else {
            $(element).addClass(errorClass).removeClass(validClass);
            $(element).closest('.form-group').removeClass('has-success').addClass('has-error');
        }
    },
    unhighlight: function (element, errorClass, validClass) {
        if (element.type === 'radio') {
            this.findByName(element.name).removeClass(errorClass).addClass(validClass);
        } else {
            $(element).removeClass(errorClass).addClass(validClass);
            $(element).closest('.form-group').removeClass('has-error').addClass('has-success');
        }
    }
});

$(function() {
    $('span.field-validation-valid, span.field-validation-error').each(function () {
        $(this).addClass('help-block');
    });
});

Im Prinzip habe ich wieder nur die CSS-Klassen angepasst und aus den Kommentaren die letzte Methode gezogen, um die Bootstrap-CSS-Klasse für die Hinweise anzufügen.
Damit das ganze auch funktioniert, muss darauf geachtet werden, dass der Code nach dem Laden der jQuery-Methoden erfolgt. Dazu habe ich in der BundleConfig.cs einfach das ScriptBundle angepasst:

bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
	"~/Scripts/jquery.unobtrusive*",
	"~/Scripts/jquery.validate*").Include("~/Scripts/wys.jquery.validate.bootstrap3.js"));

Ich habe die Datei bewusst mit einem Prefix versehen, denn nur mit dem Namen „jquery.validate.bootstrap3.js“ wäre die Datei vor den anderen jquery.validate-Javascript-Dateien geladen worden und hätte zu einem Javascript-Fehler geführt.

ASP.NET MVC & Bootstrap 3.0: ValidationSummary stylen und Felder markieren

Update: Es gab noch einen kleinen Bug im Code, den ich noch mal eben behoben habe!


Ursprünglicher Beitrag:

Ich habe mal spaßeshalber ein MVC Projekt angefangen und dort ebenfalls das vor wenigen Tagen erschienene Bootstrap 3 implementiert. Nun wollte ich die bekannte ValidationSummary von MVC ein wenig mit Bootstrap stylen, jedoch scheint dies nicht so einfach.

Bei stackoverflow (wo auch sonst) bin ich auf eine eigene Implementierung der ValidationSummary-Ausgabe speziell für Bootstrap gestoßen. Diese ist jedoch nur für die Server-seitige Validierung geeignet, und da ich mich sowieso noch nicht entschlossen hatte, welche Validierung ich nutzen möchte, habe ich kurzerhand die Client-seitige deaktiviert.

Neben der bootstrap-gestylten ValidationSummary zeigte „PeteGo“ noch, wie er seine Eingabefelder für den Benutzer fehlerhaft markiert. Dies ist mir allerdings zu aufwendig, da man natürlich mit jedem neuen Feld auch auch an die Prüfung denken muss. Ja ich weiß, ich kann ja Editor-Templates nutzen, will ich aber nicht 😉

Da ich die Erweiterung von PeteGo sowieso anfassen musste, sie war nur für Bootstrap 2 geeignet, habe ich die einzelnen Fehlermeldungen auch noch einem data--Attribute versehen und markiere per jQuery die Felder als fehlerhaft.

Die Erweiterung:

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.Web.Mvc
{
    public static class HtmlExtensionMethods
    {
        /// <summary>
        /// Returns an error alert that lists each model error, much like the standard ValidationSummary only with
        /// altered markup for the Twitter bootstrap styles.
        /// </summary>
        public static MvcHtmlString ValidationSummaryBootstrap(this HtmlHelper helper, bool closeable)
        {
            string errorMessage = "Please fix the errors listed below and try again.";
            string validationTitle = "Validation error";

            ModelStateDictionary modelErrors = helper.ViewContext.ViewData.ModelState;

            if (!modelErrors.Any(error => error.Value.Errors.Any()))
            {
                return new MvcHtmlString(string.Empty);
            }

            StringBuilder divContent = new StringBuilder();
            
            if (closeable)
            {
                TagBuilder button = new TagBuilder("button");
                button.AddCssClass("close");
                button.MergeAttribute("type", "button");
                button.MergeAttribute("data-dismiss", "alert");
                button.SetInnerText("x");
                divContent.Append(button);
            }

            TagBuilder validationTitleTag = new TagBuilder("strong");
            validationTitleTag.SetInnerText(validationTitle);
            divContent.Append(validationTitleTag);

            divContent.Append(" ");
            divContent.Append(errorMessage);

            StringBuilder ulContent = new StringBuilder();
            
            foreach (KeyValuePair<string, ModelState> modelError in modelErrors)
            {
                string id = modelError.Key;
                foreach(ModelError item in modelError.Value.Errors)
                {
                    TagBuilder li = new TagBuilder("li");
                    li.SetInnerText(item.ErrorMessage);
                    li.MergeAttribute("data-bootstrap-has-error", id);
                    ulContent.Append(li);
                }
            }
            TagBuilder ul = new TagBuilder("ul") {InnerHtml = ulContent.ToString()};
            divContent.Append(ul);

            TagBuilder div = new TagBuilder("div");
            div.AddCssClass("alert");
            div.AddCssClass("alert-error");
            div.AddCssClass("alert-block");
            div.InnerHtml = divContent.ToString();

            return new MvcHtmlString(div.ToString());
        }

        /// <summary>
        /// Overload allowing no arguments.
        /// </summary>
        public static MvcHtmlString ValidationSummaryBootstrap(this HtmlHelper helper)
        {
            return ValidationSummaryBootstrap(helper, true);
        }
    }
}

Und nun die paar Zeilen Javascript, welche dann die Felder entsprechend markieren:

$(function () {
    $('div.alert').each(function () {
        $(this).find('li[data-bootstrap-has-error]').each(
            function () {
                var id = $(this).attr('data-bootstrap-has-error');
                $('input[id=' + id + '],select[id=' + id + ']').parent().addClass('has-error');
            });
    });
});

Vielleicht kann es ja jemand gebrauchen 🙂

Ach ja, falls jemand ein paar Pros / Contras bezüglich Client-/Server-Validierung hat, kann er diese gerne in den Kommentaren hinterlassen.