Archiv für den Monat Juli 2014

ASP.NET MVC eigener JsonConverter für Decimal Datentyp mit CultureInfo


Eigentlich habe ich bereits einen Beitrag geschrieben, in dem ich auf die Problematik “…Datetime JSON Serialize…” eingegangen bin. Ich bin aber erneut auf ein ähnliches Problem gestoßen und habe noch einmal etwas detaillierter recherchiert und fand eine vergleichbare Lösung für Datetime und das Serialisieren des richtigen Dezimaltrennzeichens. Das ganze funktioniert jetzt mit einem Benutzerdefinierten JsonConverter und einem “eigenen” Actionresult, das ganze wird dann über eine Controllerextension aufgerufen und wir leiten nicht extra vom Controller ab wie in meiner “alten” Lösung.

Um die passenden Dezimaltrenneichen zu serialisieren ob z.B. Punkt “.” oder Komma “,” verwendet werden soll benötigen wir einen eigenen JsonConverter, den wir später unserem Serializer hinzufügen. Diesem Konverter übergeben wir die aktuelle CultureInfo für die der Wert ausgegeben werden soll und .NET weiß dann von “allein” welches Trennzeichen für die jeweilige Culture benutzt werden muss. Dann müssen wir noch angeben für welche Datentypen der Konverter verwendet werden soll, in unserem Fall sind das alle Datentypen die Dezimaltrennzeichen verwenden, wie z.B. “decimal”, “double” und “float”.

public class FormattedDecimalConverter : JsonConverter
{
    private CultureInfo culture;

    public FormattedDecimalConverter(CultureInfo culture)
    {
        if (culture == null)
        {
            culture = new CultureInfo("De-de");
        }

        this.culture = culture;
    }

    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(decimal) ||
                objectType == typeof(double) ||
                objectType == typeof(float));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(Convert.ToString(value, culture));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Diesen JsonConverter verwenden wir dann in unserem Benutzerdefinierten ActionResult, in dem wir den JsonSerializerSettings den passenden Konverter hinzufügen. Dabei gibt es bereits einige Vorgefertigte Konverter wie z.B. den “IsoDateTimeConverter” der das Datum im ISO Format serialisiert oder den “StringEnumConverter” der beim Serialisieren nicht den Int Wert des Enum serialisiert sondern den Namen des jeweiligen Enum Wertes. Man kann beliebig viele Konverter den JsonSerializerSettings hinzufügen um ein Json Objekt ganz nach den eigenen Maßstäben zu erstellen. Hier sollte man aber aufpassen, das .NET die entsprechenden Datentypen auch bei einem Postback wieder “zurückverwandeln” kann in den entsprechenden Datentyp, denn dafür muss sonst noch ein eigener Modelbinder geschrieben werden.

/// <summary>
/// Benutzerdefiniertes Json Result um z.B. Dezimalwerte Länderspezisch zurückzugeben
/// http://yobriefca.se/blog/2010/11/20/better-json-serialisation-for-asp-dot-net-mvc/
/// </summary>
public class JsonNetResult : ActionResult
{
    #region Konstruktor
    public JsonNetResult()
    {
    }

    public JsonNetResult(object data)
    {
        Data = data;
    }

    public JsonNetResult(object data, CultureInfo culture)
    {
        CultureInfo = culture;
        Data = data;
    }

    public JsonNetResult(object data, JsonSerializerSettings settings)
        : base()
    {
        Data = data;
        Settings = settings;
    }
    #endregion

    #region Properties
    /// <summary>
    /// Die aktuellen Ländereinstellungen Default ist "De-de", z.B. für das Umwandeln
    /// von Dezimalzahlen notwendig
    /// </summary>
    public CultureInfo CultureInfo { get; set; }

    /// <summary>
    /// Gets or sets the serialiser settings
    /// </summary>
    public JsonSerializerSettings Settings { get; set; }

    /// <summary>
    /// Gets or sets the encoding of the response
    /// </summary>
    public Encoding ContentEncoding { get; set; }

    /// <summary>
    /// Gets or sets the content type for the response
    /// </summary>
    public string ContentType { get; set; }

    /// <summary>Gets or sets the body of the response</summary>
    public object Data { get; set; }

    /// <summary>
    /// Gets the formatting types depending on whether we are in debug mode
    /// </summary>
    private Formatting Formatting
    {
        get
        {
            return Debugger.IsAttached ? Formatting.Indented : Formatting.None;
        }
    }
    #endregion

    #region Override
    /// <summary>
    /// Serialises the response and writes it out to the response object
    /// </summary>
    /// <param name="context">The execution context</param>
    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        HttpResponseBase response = context.HttpContext.Response;
        response.ContentType = !string.IsNullOrEmpty(ContentType) ? ContentType : "application/json";

        // set content encoding
        if (ContentEncoding != null)
        {
            response.ContentEncoding = ContentEncoding;
        }

        //Setzen der passenden Sprache
        if (CultureInfo == null)
        {
            CultureInfo = new CultureInfo("De-de");
        }

        if (Settings == null)
        {
            Settings = new JsonSerializerSettings();
            //Setzen der Standard Converter um z.B. bei Dezimalzahlen
            //statt "." eine "," als Seperator zu verwenden
            Settings.Converters.Add(new FormattedDecimalConverter(CultureInfo));
            //Den passenden Datetime Konverter einstellen
            //http://james.newtonking.com/archive/2009/02/20/good-date-times-with-json-net
            Settings.Converters.Add(new IsoDateTimeConverter());
            //Enum Converter einstellen, das nicht Int sondern der String versendet wird
            //Settings.Converters.Add(new StringEnumConverter());
        }

        if (Data != null)
        {
            JsonTextWriter writer = new JsonTextWriter(response.Output) { Formatting = Formatting };
            JsonSerializer serializer = JsonSerializer.Create(Settings);
            serializer.Converters.Add(new FormattedDecimalConverter(CultureInfo));

            serializer.Serialize(writer, Data);
            writer.Flush();
        }
    }
    #endregion
}

Das ganze kann dann sehr einfach mit Hilfe einer kleinen Extensionmethode für den Controller verwendet werden

/// <summary>
/// Controller Extensions um einfacher auf unser Custom JsonResult zugreifen zu können.
/// </summary>
public static class ControllerExtensions
{
    /// <summary>
    /// Umwandeln der übergebenen Daten in ein passenden JSON Objekt.
    /// </summary>
    /// <param name="data">Die Daten die umgewandelt werden sollen</param>
    public static JsonNetResult JsonEx(this Controller controller, object data)
    {
        return new JsonNetResult(data);
    }

    /// <summary>
    /// Umwandeln der übergebenen Daten in ein passenden JSON Objekt.
    /// </summary>
    /// <param name="data">Die Daten die umgewandelt werden sollen</param>
    /// <param name="culture">Die Länderinformationen die bei der Umwandlung verwendet werden sollen</param>
    public static JsonNetResult JsonEx(this Controller controller, object data, CultureInfo culture)
    {
        return new JsonNetResult(data, culture);
    }

    /// <summary>
    /// Umwandeln der übergebenen Daten in ein passenden JSON Objekt.
    /// </summary>
    /// <param name="data">Die Daten die umgewandelt werden sollen</param>
    /// <param name="settings">Die Serializer Settings die man hier Individuell angeben kann</param>
    public static JsonNetResult JsonEx(this Controller controller, object data, JsonSerializerSettings settings)
    {
        return new JsonNetResult(data, settings);
    }
}

Im Controller selbst sieht der Aufruf, dann z.B. folgendermaßen aus

 public ActionResult GetModelData()
 {
     var obj = new ServiceData()
     {
         Alter = 32,
         Geburtstag = new DateTime(1982, 3, 15),
         Gehalt = (decimal) 1034.45,
         Name = "Johannes"
     };

     return this.JsonEx(obj);
 }
Advertisements

AngularUI – benutzerdefinierte Direktive für eine Multiselect Dropdownliste im Bootstrap Style inkl. AngularJS Form Validation


Leider war ich vergeblich auf der Suche nach einer brauchbaren Lösung für eine Multiselect Dropdownliste für AngularJS im Twitter Bootstrap Style die meinen Anforderungen entspricht. Daher habe ich AngularUI als Vorlage für meine “eigene” Mutliselect Dropdownliste genommen und von AngularUI die Dropdown Komponente als Grundlage verwendet. Denn die Dropdownkomponente bietet schon alle wichtigen Grundlagen für eine Multiselect Dropdownliste. Das ganze habe ich dann in eine eigene Direktive verpackt und kann entsprechend in AngularJS verwendet werden.

Voraussetzungen und was die Multiselect Dropdownliste alles können soll:

  • Benötigt wird AngularJS und  AngularUI als Grundlage (daher wird auch kein jQuery benötigt!)
  • Das Basic Styling übernimmt Twitter Bootstrap, aber die Icons verwende ich von Font Awesome
  • Ein paar Custom Styles um die Dropdown Komponenten entsprechend anzupassen für die Multiselect Liste
  • Automatisches Anpassen der Breite des Controls an z.B. “col-md-12” von Bootstrap
  • eine eigene Direktive inkl. Template für die Multiselect Liste
  • Integrierte Form Validation für das “required” Attribut sowie “$dirty” und “$pristine” Modus des Formulars in dem die Liste verwendet wird
  • Die Möglichkeit alle Elemente an und abzuwählen
  • Über Änderungen der Auswahl “bescheid” geben wie bei “ng-change”

1. Custom Styles

/* Multiselect Dropdown Styles - Start */
.sq-dropdown .dropdown-multiselect {
	max-height: 300px; 
	overflow-y: auto; 
	overflow-x: hidden;
	width: 100%;
}

.sq-dropdown .ddl-icon {
    display: inline-block;
    margin-top: 2.5px;
    position: absolute;
    right: 15px;
}

.sq-dropdown.bootstrap-select:not([class*="span"]):not([class*="col-"]):not([class*="form-control"]), .bootstrap-select > .btn {
    width: 100%;
}

.sq-dropdown.bootstrap-select.btn-group .btn .filter-option {
    left: 12px;
    overflow: hidden;
    position: absolute;
	text-overflow:ellipsis;
    right: 25px;
    text-align: left;
}

.sq-dropdown.bootstrap-select.btn-group .btn .caret {
    margin-top: -2px;
    position: absolute;
    right: 12px;
    top: 50%;
    vertical-align: middle;
}
/* Multiselect Dropdown Styles - Ende */

2. Benutzerdefinierte Direktive

wichtig ist hier, das die Liste die angezeigt werden soll, die Properties “Selected” und “Text” enthält. Denn das Property “Text” wird angezeigt und in “Selected” merken wir uns welches Element ausgewählt wurde. Die Funktionen “validators” und das $watch für “listEntries” wird beides benötigt, damit das Control mit der Form Validierung von AngularJS funktioniert. Denn wenn sich die Auswahl ändert oder das “required” Attribut gesetzt wird, kann man über die Standard AngularJS Methoden prüfen ob die Eingaben valide sind z.B. “frm.$valid” oder “frm.$dirty” oder auch “frm.$pristine” oder auch direkt das Control ansprechen. Damit die Validierung überhaupt verwendet werden kann, muss auch “required: ‘ngModel’ gesetzt, werden denn nur dann steht uns das Model in der Link Funktion zur Verfügung und wir können auf “$setValidity” zugreifen für die “required” Validierung.

Damit das alles Funktioniert, darf man natürlich nicht vergessen, dem Control das “name” Attribut zu verpassen inkl. eines passenden Namens.

angular.module("select.directives", [])
    /*
    Direktive zum Anzeigen einer MultiSelect Box als Dropdownliste. Achtung Es wird hier ebenfalls AnguluarUi benötigt!
    Es werden die Properties "Selected" und "Text" benötigt in der übergebenen Anzeigeliste, damit die Multiselect Liste richtig funktioniert.

    Verwendung:

    html ...
    <div sq-multiselect ng-model="ModelData.Listendaten" value-changed="LogChange(selectedValue)" required></div>
    ... html
    */
    .directive("sqMultiselect", function () {
        return {
            restrict: 'AE',
            replace: true,
            require: 'ngModel',
            scope: {
                listEntries: "=ngModel", //unsere Liste mit den Einträgen
                valueChanged: "&" //Funktion die aufgerufen wird, wenn sich der Wert geändert hat.
            },
            //ACHTUNG funktioniert nur in Verbindung mit AngularUi und dem dort enthaltenen "dropdown" Control!
            //dies muss auch mit in den Abhängigkeiten geladen werden mit "ui.bootstrap".
            template: '<div class="sq-dropdown bootstrap-select btn-group" dropdown>' +
                '<button type="button" class="btn btn-default dropdown-toggle" title="{{Fct.selectedString(listEntries)}}">' +
                '<span class="filter-option" ng-bind="Fct.selectedString(listEntries)"></span>&nbsp;' +
                '<span class="caret"></span></button>' +
                '<ul class="dropdown-menu dropdown-multiselect" role="menu">' +
                '<li ng-show="listEntries.length > 0"><a href="" ng-click="Fct.selectAll(listEntries)">' +
                '<span style="margin-right: 30px;"><strong ng-bind="Locals.selectAll"></strong></span>' +
                '<i ng-show="Fct.allSelected(listEntries)" class="fa fa-check ddl-icon"></i>' +
                '</a>' +
                '</li>' +
                '<li ng-repeat="item in listEntries">' +
                '<a ng-click="item.Selected = !item.Selected" href="">' +
                '<span style="margin-right: 30px;" ng-bind="item.Text"></span> ' +
                '<i ng-show="item.Selected" class="fa fa-check ddl-icon"></i>' +
                '</a>' +
                '</li>' +
                '</ul>' +
                '</div>',
            link: function(scope, elem, attr, ngModel) {
                scope.Fct = {};
                scope.Locals = {};
                scope.Locals.selectAll = "Alle auswählen";

                //String zusammenbauen für die ausgewählten Elemente
                scope.Fct.selectedString = function(data) {
                    var anz = "-- Bitte wählen --", count = 0;
                    if (data !== undefined && data !== null) {
                        for (var i = 0; i < data.length; i++) {
                            //Nur wenn das Element auch ausgewählt wurde anzeigen.
                            if (data[i].Selected) {
                                if (count === 0) {
                                    anz = "";
                                }
                                if (anz.length > 0) {
                                    anz += "; ";
                                }
                                //Text zusammenbauen
                                anz += data[i].Text;
                                count++;
                            }
                        }
                    }
                    return anz;
                };

                //Funktion die alle Werte auswählt und wenn alle Werte ausgewählt wurden, dann
                //werden alle wieder abgewählt
                scope.Fct.selectAll = function(data) {
                    var select = true;
                    //Wenn alle ausgewählt sind, dann alle abwählen.
                    if (scope.Fct.allSelected(data)) {
                        select = false;
                    }
                    if (data !== undefined && data !== null) {
                        for (var i = 0; i < data.length; i++) {
                            data[i].Selected = select;
                        }
                    }
                };

                //Gibt zurück ob alle Werte in der Liste ausgewählt wurden.
                scope.Fct.allSelected = function(data) {
                    if (data !== undefined && data !== null) {
                        for (var i = 0; i < data.length; i++) {
                            if (!data[i].Selected) {
                                return false;
                            }
                        }
                    }
                    return true;
                };

                //Gibt True zurück, wenn kein Wert in der Liste ausgewählt wurde.
                scope.Fct.noneSelected = function(data) {
                    if (data !== undefined && data !== null) {
                        for (var i = 0; i < data.length; i++) {
                            if (data[i].Selected) {
                                return false;
                            }
                        }
                    }
                    return true;
                }

                //Custom Validation einbinden für Required - Das value ist hier ngModel was immer übergeben wird.
                //vom $parsers bzw. $formatters
                var validator = function (value) {
                    //Einbinden einer Required Validierung, es wird das "required" Attribut unterstützt
                    //und nur wenn auch ein Wert im ngModel (selectedValue) gesetzt ist, ist die Validation True!
                    if (attr.required !== undefined) {
                        if (scope.Fct.noneSelected(value)) {
                            ngModel.$setValidity("required", false);
                        } else {
                            ngModel.$setValidity("required", true);
                        }
                    }
                    //WICHTIG den Eingabewert auch zurückgeben, sonst landet im Model kein Wert mehr!
                    return value;
                }

                //Unseren Validator den passenden Listen hinzufügen.
                ngModel.$parsers.unshift(validator);    //view-to-model direction
                ngModel.$formatters.unshift(validator); //model-to-view direction

                //Prüfen ob sich die Liste ändert
                //http://www.bennadel.com/blog/2566-scope-watch-vs-watchcollection-in-angularjs.htm
                scope.$watch('listEntries', function (newVal, oldVal) {
                    if (newVal !== undefined && newVal !== oldVal) {
                        //Wenn die Werte unterschiedlich sind, dann ebenfalls die Methode für Change aufrufen!
                        scope.valueChanged();
                        //Wenn der Wert einfach nur "geändert" wird, muss auch $dirty und $pristine "gesetzt" werden
                        //dies passiert automatisch über "ngModel.$setViewValue(ngModel.$viewValue);"
                        ngModel.$setViewValue(ngModel.$viewValue);
                    }
                }, true); //TRUE NOTWENDIG!
            }
        };
    })

3. Verwendung

JavaScript

var app = angular.module("app.ddl", ["ui.bootstrap", "select.directives"]);

app.controller("ddlCtrl", function ($scope, $log) {
    $scope.multiSelectItems = {
        users: [
            {
                Text: "Ray Jones",
                Selected: true,
                joined: 2012
            },
            {
                Text: "Lana Lane",
                Selected: false,
                joined: 2001
            },
            {
                Text: "Titus Jonas",
                Selected: true,
                joined: 1995
            },
            {
                Text: "Serena Wuh",
                Selected: true,
                joined: 1990
            }
        ]
    };

    $scope.Fct = {};
    $scope.Fct.LogChange = function(entry) {
        $log.log(entry);
    }

});

HTML

<div ng-app="app.ddl" ng-controller="ddlCtrl">
    <form name="frm">
        <div class="row">
            <div class="col-md-12"><strong>Multiselect mit AngularUi</strong></div>
        </div>
        <div class="row">
            <div class="col-md-3">
                <div required name="multiPersonSelect" sq-multiselect ng-model="multiSelectItems.users" value-changed="Fct.LogChange(multiSelectItems.users)"></div>
            </div>
        </div>
    </form>
</div>

 

Das ganze kann natürlich beliebig erweitert werden. Auf meinem CodePlex Account findet Ihr die Quellcodes und eine funktionierende MVC Anwendung. Außerdem ist dort auch noch eine Implementierung einer einfachen Select Dropdown Liste im Bootstrap Style wie die Multiselect Liste vorhanden.

Ich weiß leider nicht ob meine Umsetzung dem Standard entspricht, denn ich habe leider kein ähnliches Control gefunden, welches ebenfalls die Form Validation von AngularJS unterstützt. Über Vorschläge und Hinweise freue ich mich sehr :-)