Archiv der Kategorie: HTML

CopyToClipboard Directive mit AngularJs, TypeScript und ControllerAs Syntax


Bisher war Flash notwendig um auf einer Webseite einen Button anzubieten, der einen Text in die Zwischenablage kopiert. Wenn man aber das Glück hat und bei seiner Anwendung nur die neuesten Browser unterstützen muss, kann man jetzt auch auf eine native JavaScript Funktion zurückgreifen.

$("#inputText").select();
document.execCommand("copy");

Denn man kann mit diesen zwei Zeilen Code ab Chrome:42+, FF:41+, IE9+ und Opera29+ (Safari:nicht unterstützt) den Text z.B. in einer Textbox auswählen, den man dann in die Zwischenablage kopieren möchte.

Meine Direktive kann folgendermaßen verwendet werden in einem “div” oder jedem beliebigen anderen Element:

<div sq-copy-to-clipboard ng-model="ctrl.viewModel.Name" sq-call-back-fct="ctrl.DoSomething" sq-icon-css="'fa fa-fw fa-copy'" sq-btn-css="'btn btn-default btn-xs'" sq-title="'In die Zwischenablage'"></div>

In der AngularJS Direktive “sqCopyToClipboard” möchte ich aber nur einen Button anbieten der ein ngModel Element enthält und dessen Wert in der Zwischenablage ablegt. Damit dies funktioniert, wird mit Hilfe von jQuery jedes mal wenn der Button zum Kopieren des ngModel Wertes geklickt wird, ein Temporäres HTML Textfeld erstellt was nur 1x1px groß ist und am Ende des DOMs angehängt wird. Dann wird der Text darin markiert und kopiert und danach wird das Element auch gleich wieder aus dem DOM entfernt. Das Element muss 1x1px groß sein, damit Chrome den Text darin auch markieren kann. Denn bei 0x0px größe kann Chrome keinen Text markieren.

In der Direktive selbst verwende ich die “controllerAs” Syntax, die mit AngularJs 1.2 eingeführt wurde und die neueste Version von “bindToController”, die seit AngularJs 1.4 existiert. Der $scope wird damit nicht mehr benötigt und muss auch nicht mehr in der Controllerfunktion injected werden. Denn auch in Direktiven kann jetzt genau wie beim Controller die “ControllerAs” Syntax verwendet werden. Damit kann man im Template der Direktive über den “ControllerAs” Namen z.B. “sqCopyPasteCtrl” auf die Variablen zugreifen die man in “bindToController” definiert hat und auch auf alle weiteren Variablen und Funktionen die man in der Controllerfunktion über “this” zur Verfügung stellt. Zur genauen Funktionsweise von “controllerAs” kann ich nur den Blogeintrag “Exploring Angular 1.3 Binding to Directive Cotnrollers” empfehlen.

HTML Template meiner Directive Link “a” mit Icon Class:

<a ng-click="sqCopyPasteCtrl.copyToClipboard()" ng-class="sqCopyPasteCtrl.sqBtnCss" title="{{sqCopyPasteCtrl.sqTitle}}">
<i ng-class="sqCopyPasteCtrl.sqIconCss"></i></a>

Die Direktive selbst in TypeScript verfasst.

module App.Directives {
    /* 
    * Definition der Scope Variablen
     */
    export interface ICopyToClipboardScope {
        sqValues : string;
        sqTitle : string;
        sqIconCss : string;
        sqBtnCss: string;
        sqCallBackFct;
    }

    /*
     * Directive, die den String im ngModel in die Zwischenablage kopiert. 
     * https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
     * ACHTUNG geht nur ab Chrome:42+, FF:41+, IE:9+, Opera:29+, Safari:Not Supported
     *
     * Verwendung: 
     *</pre>
<div></div>
<pre>
     */
    export class CopyToClipboard implements ng.IDirective {
        public restrict: string = "A";
        public replace: boolean = true;
        public require = "ngModel";
        public templateUrl: string = 'ScriptsApp/directives/templates/copyToClipboard.directives.html';
        public scope = {}

        public controller = CopyToClipboardCtrl;
        public controllerAs = "sqCopyPasteCtrl";
        public bindToController: ICopyToClipboardScope = {
            sqValues: "=ngModel",    //Der Wert der in die Zwischenablage kopiert werden soll.
            sqTitle: "=",            //Der Titel der angezeigt werden soll Optional
            sqIconCss: "=",          //CSS für das Icon
            sqBtnCss: "=",           //CSS für den Button
            sqCallBackFct: "&"        //Callback Funktion die ausgeführt wird, wenn diese gesetzt wurde.
        }

        constructor() { }

        //#region Angular Module Definition
        private static _module: ng.IModule;
        /**
        * Stellt die Angular Module für CopyToClipboard bereit.
        */
        public static get module(): ng.IModule {
            if (this._module) {
                return this._module;
            }

            //Hier die abhängigen Module für unsere Direktive definieren.
            this._module = angular.module('copyToClipboard.directives', [CopyToClipBoardServiceProvider.module.name]);
            this._module.directive('sqCopyToClipboard', [() => { return new CopyToClipboard(); }]);
            return this._module;
        }
        //#endregion
    }

    /*
    * Implementierung unseres CopyToClipboard Controllers.
    */
    export class CopyToClipboardCtrl implements ICopyToClipboardScope {
        public sqValues: any;
        public sqTitle: string;
        public sqIconCss: string;
        public sqBtnCss: string;
        public sqCallBackFct;

        static $inject = ['copyToClipBoardConfig'];

        /* 
        * Da wir die CSS Klassen für einen Provider setzen, hier den passenden Provider injecten und im Template dann auf dessen Config Werte zugreifen.
        */
        constructor(private copyToClipBoardConfig: ICopyToClipBoardServiceProvider) {
            this.init();
        }

        init(): void {
            //Prüfen ob ein Titel oder andere CSS Klasse übergeben wurde, sonst den Wert aus dem Provider verwenden
            if (this.sqTitle === undefined) {
                this.sqTitle = this.copyToClipBoardConfig.config.title;
            }

            if (this.sqBtnCss === undefined) {
                this.sqBtnCss = this.copyToClipBoardConfig.config.btnCssClass;
            }

            if (this.sqIconCss === undefined) {
                this.sqIconCss = this.copyToClipBoardConfig.config.iconCssClass;
            }
        }

        /*
        * Unseren ModelValue in die Zwischenablage kopieren.
        */
        public copyToClipboard(): void {
            var inputId: string = "sqCopyToClipboardText";
            
            //Unser Input erstellen inkl. des Textes den wir kopieren wollen, da die Angular Implementierung auf "this.sqValues" zugreift ist dies 
            //durch die Extra Definition des CopyToClipboard Controllers problemlos möglich. Das Input selbst ist Mindestens 1px breit und hoch, denn
            //sonst kann der Inhalt im Chrome nicht markiert werden, was zwingend notwendig ist damit der Inhalt kopiert werden kann.
            var input = $(``);

            try {
                //Unser Input dem DOM Hinzufügen
                $(input).appendTo(document.body);
                //Den Inhalt des Inputs auswählen, denn der execCommand kopiert nur die Inhalte in die Zwischenablage, die ausgewählt sind.
                $(`#${inputId}`, document.body).select();
                document.execCommand("copy");
            } finally {
                //Am Ende das Eingabefeld wieder aus dem DOM entfernen
                $(`#${inputId}`, document.body).remove();

                //Sollte eine CallBack Funktion angegeben sein, diese hier ausführen.
                if (this.sqCallBackFct !== undefined) {
                    this.sqCallBackFct();
                }
            }
        }
    }

    /*
     * Configklasse für unsere CopyPaste Direktive. Hier kann man z.B. die passenden Css Klassen ändern die gesetzt werden.
     */
    export class CopyToClipBoardServiceProvider implements ng.IServiceProvider, ICopyToClipBoardServiceProvider {
        //Die Konfigurationsdaten entsprechend setzen.
        config: ICopyToClipBoardConfig = {
            btnCssClass: "btn btn-default btn-sm",  //Bootstrap
            iconCssClass: "fa fa-fw fa-copy",       //Font Awesome
            title: "In die Zwischenablage kopieren"
        }

        $get = () => {
            return {
                config: this.config
            }
        };


        //#region Angular Module Definition
        private static _module: ng.IModule;
        /**
        * Stellt die Angular Module für CopyToClipboardProvider bereit.
        */
        public static get module(): ng.IModule {
            if (this._module) {
                return this._module;
            }

            //Hier die abhängigen Module für unsere Direktive definieren.
            this._module = angular.module('copyToClipBoardConfig.provider', []);
            this.module.provider("copyToClipBoardConfig", () => new CopyToClipBoardServiceProvider());
            return this._module;
        }
        //#endregion

    }

    export interface ICopyToClipBoardServiceProvider {
        config: ICopyToClipBoardConfig;
    }

    export interface ICopyToClipBoardConfig {
        btnCssClass: string;
        iconCssClass: string;
        title: string;
    }
}

Den Aktuellen Quellcode findet Ihr auch auf meinem GitHub Account.

Bootstrap 4 Alpha – Released


Das es eine neue Bootstrap Version geben wird, war nur eine Frage der Zeit. Vor allem wurde die Alpha Bereits vor einer ganzen Weile angekündigt und nun ist es endlich soweit. Eine erste Alpha Version von Bootstrap 4 wurde veröffentlicht und kann ausprobiert werden.

Sollte es noch Webentwickler geben, die nicht wissen was Bootstrap ist, dann kann ich diesen nur empfehlen, sich spätestens ab Version 4 einmal damit zu beschäftigen. Ich für meine Zwecke setze Bootstrap sehr gerne in kleinen Privaten Projekten ein oder für interne Webseiten. Denn mit Bootstrap kann man sich auf das wesentliche als Entwickler konzentrieren und dabei handelt es sich nun einmal um das Entwickeln und nicht das Stylen. Aber mit Bootstrap sieht das ganze dann trotzdem sehr elegant aus, egal wie klein das Webprojekt auch ist.

Mit Bootstrap Version 4 hat sich einiges geändert, aber zum Glück nicht komplett alles. Leider entfallen die Panels komplett und werden durch so genannte Cards ersetzt. Da ich aber ein Exzessiver Nutzer von Panels bin, muss ich hier mal genau untersuchen wie gut sich die Panels zu Cards Migrieren lassen. Sonst scheint es aber keine großen Überraschungen zu geben, was neue Controls oder Styles angeht, hier scheint beim ersten durchgehen der neuen Dokumentation vieles beim alten zu bleiben, bis auf ein paar kleine Namensanpassungen. Außerdem setzt Bootstrap 4 jetzt in der neuen Version auf Sass statt auf Less, außerdem setzt man jetzt vermehrt auf “rem” und “em” als Maßeinheit statt auf Pixel.

Daher bin ich mal gespannt wie lange der Finale Release jetzt noch auf sich warten lässt und freue mich schon meine Webseiten mit Bootstrap 4 zu erstellen.

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 :-)

Font Awesome 4.0.0 veröffentlicht (Bootstrap 3 kompatibel)


Endlich wurde auch Font Awesome in einer aktuellen Version 4.0.0 veröffentlicht. Ab dieser Version ist Font Awesome auch Bootstrap 3.0.0 kompatibel. Was soviel heißt, das beide Frameworks inzwischen auf Fonts setzten und auch gleichzeitig verwendet werden können. Font Awesome wurde für die Version 4.0.0 komplett neu geschrieben, dabei wurde wert auf eine bessere Kompatibilität und höhere Schnelligkeit gesetzt, denn in Version 4.0.0 sind nur 10 neue Icons dazugekommen.

Ich habe in einem Beispiel MVC4 Projekt Font Awesome 4 und Bootstrap 3 per nuget installiert und das ganze sieht dann in der Ordnerstruktur folgendermaßen aus:

image

Hier werden die Schriften von Bootstrap und Font Awesome problemlos im gleichen Verzeichnis abgelegt und an der prinzipiellen Verwendung von Font Awesome hat sich nichts geändert und diese können parallel zu den Bootstrap Icons verwendet werden.

Einbinden des power off Symbols in einen Link:

<i class="fa fa-power-off fa-lg"></i>
         

Leider ist es aktuell nicht mehr so leicht die Bootstrap 3 less Dateien in einem MVC Projekt einzubinden und mittels Less.js zu kompilieren (css zu erzeugen). Hier kann es zu diversen Kompilierungsproblemen kommen, daher verwende ich aktuell die CSS Dateien direkt.

Performance “verbessern” von Coded-Ui Tests für Webseiten


Die Möglichkeit auch Weboberflächen automatisiert zu Testen mit Coded-Ui, ist sehr einfach zu realisieren. Denn die Coded-Ui Oberflächentests über den IE sind ähnlich leicht umzusetzen wie Oberflächentests mit Coded-Ui für eine WPF Anwendung. Nur leider ist die Geschwindigkeit wenn man Daten aus der Oberfläche über das “BrowserWindow” Objekt abfragt sehr langsam.

Wenn man z.B. eine Tabelle mit Werten hat, die verglichen werden sollen, dann bleibt einem mit den Coded-Ui Objekten nichts anderes übrig, wie die Tabelle zu ermitteln und dann in der Tabelle z.B. über ein foreach row die einzelnen Zeilen bzw. Zellen abzufragen:

//Tabelle wurde vorher über die Coded-Ui Oberfläche direkt als Element eingebunden
HtmlTable statisticTable = UiStatisticsSummaryTbl();
//z.B. den  Inhalt aus einer Zelle Ermitteln um diesen später zu vergleichen
string text = statisticTable.GetCell(1, 1).InnerText;

Wenn man hier die Tabelle immer erneut oder auch nur einmalig nach bestimmten Werten durchsuchen muss, kann dies eine Beträchtliche Zeit in Anspruch nehmen.

Um dieses Problem zu umgehen, nutze ich für aufwendige inhaltliche Oberflächenüberprüfungen das HtmlAgilityPack und lasse mir den HTML Code des kompletten Browser Fensters zurückgeben und parse/überprüfe die Oberflächenwerte mit der Hilfe von XPath Ausdrücken.

Ermitteln des Aktuellen Browserfensterinhalts:

 /// <summary>
 /// Den aktuellen Browserfensterinhalt als HTMLDocument des Agility Packs zurückgeben.
 /// </summary>
 public static HtmlAgilityPack.HtmlDocument GetHtmlDocument(BrowserWindow window)
 {
     HtmlDocument document = new HtmlDocument(window);
     string text = (string)document.GetProperty("OuterHtml");

     if (!string.IsNullOrEmpty(text))
     {
         HtmlAgilityPack.HtmlDocument doc= new HtmlAgilityPack.HtmlDocument();
         doc.LoadHtml(text);
         return doc;
      }
            return null;
 }

Die Abfragen direkt über das HTML Dokument selbst dauern dann nur noch einen Bruchteil der Zeit wie die Coded-Ui Abfrage eines einzelnen Elements gedauert hat.

//Unser HTML ermitteln aus dem aktuellen Testbrowserfenster
HtmlAgilityPack.HtmlDocument doc = GetHtmlDocument(browserwindow);
HtmlAgilityPack.HtmlDocument node = new HtmlAgilityPack.HtmlDocument();
//Unsere Tabelle anhand der ID ermitteln
node.LoadHtml(doc.GetElementbyId("DefaultStatTSummaryTblId").InnerHtml);
//Alle Rows unserer Tabelle ermitteln (Achtung die Header tr werden auch mit ermittelt)
HtmlAgilityPack.HtmlNodeCollection rows = node.DocumentNode.SelectNodes("//tr");
//den passenden Inhalt ermitteln aus der passenden Zelle
string text = rows[1].SelectNodes(".//td")[1].InnerText;

Mit dieser Methode kann man natürlich nicht alle Oberflächentests durchführen, aber wenn es sich um aufwendige Inhaltsabfragen der Oberfläche handelt, dann kann man damit einiges an Zeit einsparen.

Bootstrap 3 RC1–Released


Nur zur Info – Twitter Bootstrap ist in der Version 3 RC1 Released worden. Sobald ich mehr Zeit habe und es nicht mehr so warm draußen ist, schaue ich mir das ganze mal im Detail an.

Was ich aber bisher bereits gesehen habe schaut sehr gut aus, vor allem die neuen “Panels” zum besseren Visualisieren von bestimmten Content Inhalten schauen sehr cool aus

image

image

Außerdem ist es jetzt auch möglich die Größe von Eingabefeldern bzw. Eingabekombinationen zu verändern.

image

image

Außerdem wirkt das ganze Layout etwas reduzierter und “flacher”, einfach gesagt etwas zeitgemäßer wenn man die aktuellen Trends im Layout verfolgt. Was man z.B. an den Buttonstyles ganz gut erkennen kann.

image

Quelle: http://twitter.github.io/bootstrap/

MVC Custom/Benutzerdefinierte Messages “verwalten” und zwischen Views und Controllern übergeben und anzeigen – v2.0


Es ist schon eine Weile her, da habe ich in “Grundzügen” in einem Blogpost gezeigt wie man Messages (Meldungen) in MVC so erweitert, das man auch eigene Meldungen anzeigen kann und nicht nur Fehlermeldungen anzeigt. Das ganze habe ich jetzt ein wenig aufgebohrt und mit Twitter Bootstrap Alert Styles umgesetzt. Mit der neuen Methode, ist es möglich gleichzeitig beliebig viele Fehlermeldungen und Messages anzuzeigen, die zum einen z.B. aus dem Modelstate Fehlern zusammengesetzt werden und aus den eigenen Meldungen bestehen. Die eigenen Meldungen müssen hier keine Fehler sein, es kann sich z.B. um Positive Bestätigungen oder Warnmeldungen handeln.

image

Das ganze setzt sich aus einem HTML Helper zusammen der einfach im View aufgerufen wird und die Meldungen dann nur noch darstellt. Die eigenen Meldungen, die wir ausgeben wollen, werden dabei im TempData abgelegt. Diese Variable wir automatisch gelöscht, nach dem Sie einmal abgerufen wurde und wir müssen uns um “nichts” kümmern. Wobei die Messages durch die Modelvalidation, werden immer als Fehler dargestellt und aus dem Modelstate abgerufen.

/// <summary>
/// Prüft ob im TempData Messages hinterlegt wurden und wenn ja dann werden diese angezeigt.
/// nach einem Request ist Tempdata auch direkt wieder leer, ist nur für einen Request aktiv!
/// Außerdem wird überprüft ob die Modeldaten ebenfalls einen Fehler enthalten und dieser wird hier ebenfalls ausgegeben
/// 
/// http://stackoverflow.com/questions/4642845/asp-net-mvc-how-to-preserve-modelstate-errors-across-redirecttoaction
/// </summary>
/// <returns>HTML Konstrukt was eine Custom Message anzeigt.</returns>
public static MvcHtmlString ShowCustomMessage(this HtmlHelper helper)
{
    //Eine neue Messageliste initialisieren
    List<Message> messages = new List<Message>();

    //Prüfen ob der passende Einträge im TempData vorhanden sind, die wird dort selbst abgelegt haben
    if (helper.ViewContext.Controller.TempData[CustomMessage.TempMessageString] != null)
    {
        //den Tempdata Liste der Nachrichten abrufen
        messages = (List<Message>)helper.ViewContext.Controller.TempData[CustomMessage.TempMessageString];
    }

    //Nur prüfen ob Fehler im Modelstate sind, wenn dieser auch nicht valide ist.
    if (!helper.ViewData.ModelState.IsValid)
    {
        //Nach Meldungen die Vom System im Modelstatus abgelegt wurden suchen
        //Ebenfalls den aktuellen Modelstatus nach meldungen durchsuchen, dabei handelt es sich immer um Fehlermeldungen!
        foreach (var key in helper.ViewData.ModelState.Keys)
        {
            foreach (var err in helper.ViewData.ModelState[key].Errors)
            {
                //Bei den Meldungen im Modelstate handelt es sich immer um einen Fehler. Die Fehler dem Modelstate hinzufügen
                messages.Add(new Message(helper.Encode(err.ErrorMessage), MessageTypes.error));
            }
        }    
    }

    string html = string.Empty;
    //Unsere Messages erstellen. Dabei wird jede Message in einer eigenen Fehlermeldung ausgegeben und sortiert ausgeben von Fehler nach OK
    foreach (Message message in messages.OrderByDescending(p => p.MessageType))
    {
        //Festlegen um welchen Fehlertyp es sich handelt
        string errorClass = GetMessageTypeCssString(message.MessageType);

        //Den HTML String zusammenbauen mit den Messages
        html += string.Format("<div class=\"{0}\"><button class=\"close\" data-dismiss=\"alert\" type=\"button\">×</button>{1}</div>", errorClass, message.Text);
    }

    //Damit der HTML String auch als HTML auf der Webseite ausgegeben werden kann, diesen noch umwandeln und den Rückgabewert des Helpers anpassen.    
    return new MvcHtmlString(html);
}

/// <summary>
/// Gibt den passenden CSS Code für den übergebenen Messagetype zurück
/// </summary>
/// <param name="message">Der MessageType für den der passende CSS Code zurück gegeben werden soll</param>
private static string GetMessageTypeCssString(MessageTypes message)
{
    string errorClass = string.Empty;
    switch (message)
    {
        case MessageTypes.error:
            errorClass = "alert alert-error";
            break;
        case MessageTypes.warning:
            errorClass = "alert";
            break;

        case MessageTypes.ok:
            errorClass = "alert alert-success";
            break;

        case MessageTypes.message:
            errorClass = "alert alert-info";
            break;
        default:
            errorClass = "alert";
            break;
    }

    return errorClass;
}

Die eigenen Messages setzten sich dabei aus einem Messagestring und einem Messagetyp zusammen. Der Messagetyp gibt an um welche Art von Meldung es sich handelt, z.B. Fehler, Warnung, Ok oder Meldung.

/// <summary>
/// Gibt an, welcher MessageType auf der Oberfläche angezeigt werden soll, wird im Viewbag abgelegt.
/// </summary>
public enum MessageTypes
{
    ok,
    warning,
    error,
    message
}

/// <summary>
/// Wird zum Darstellen von Nachrichten auf der Webseite benötigt, wenn diese über Views hinweg "versendet" werden.
/// </summary>
public class Message
{
    public string Text { get; set; }
    public MessageTypes MessageType { get; set; }

    /// <summary>
    /// Konstruktor für die Message
    /// </summary>
    /// <param name="text">Der Text der angezeigt werden soll</param>
    /// <param name="messageTypes">Um die Art der Nachricht die angezeigt werden soll</param>
    public Message(string text, MessageTypes messageTypes)
    {
        MessageType = messageTypes;
        Text = text;
    }
}

Das Verwalten der CustomMessages wird mit der folgenden Klasse umgesetzt. Wichtig ist hier, das wir immer den Aktuellen Controller mit übergeben müssen, damit wir auf TempData zugreifen können um die Messages hinzufügen zu können. Im Tempdata befindet sich nur eine Liste mit den jeweils von uns zuletzt hinzugefügten CustomMessages und wenn wir eine neue Message hinzufügen, wird die Liste um diese Meldung erweitert.

/// <summary>
/// Custom Message, damit Nachrichten auch über Views hinweg dargestellt werden können.
/// </summary>
public class CustomMessage
{
    #region static
    /// <summary>
    /// Gibt eine Instanz von CustomMessage zurück um Benutzerdefinierte Messages über Views hinweg anzeigen zu können.
    /// http://stackoverflow.com/questions/4642845/asp-net-mvc-how-to-preserve-modelstate-errors-across-redirecttoaction
    /// </summary>
    /// <param name="controller">Der CurrentController damit auf TempData zugegriffen werden kann.</param>
    /// <returns></returns>
    public static CustomMessage CurrentMessage(Controller controller)
    {
        return new CustomMessage(controller);
    }
    #endregion

    #region Member
    /// <summary>
    /// Constante die für den Tempdata[TempMessageString] benutzt wird.
    /// </summary>
    public const string TempMessageString = "CMTempMessageString";

    /// <summary>
    /// Der aktuelle CurrentController um auf 
    /// </summary>
    public Controller CurrentController { get; set; }
    #endregion

    #region Konstruktor
    /// <summary>
    /// Initialisieren der Methode, der CurrentController wird benötigt damit man auf das TempData zurückgreifen kann
    /// </summary>
    /// <param name="controller">Der aktuelle CurrentController damit TempData verwendet werden kann</param>
    private CustomMessage(Controller controller)
    {
        CurrentController = controller;
    }
    #endregion

    #region Public Functions
    /// <summary>
    /// Hinzufügen der Custommessage zu den TempData, damit dieser Mittels des passendne HTMLHelpers 
    /// auf der passenden Seite angezeigt werden kann.
    /// </summary>
    /// <param name="message">Die Message die angezeigt werden soll</param>
    /// <param name="messageType">Der Messagetyp der angezeigt werden soll</param>
    public void AddMessage(string message, MessageTypes messageType)
    {
        if (message != null)
        {
            //Eine Liste erstellen, damit wird jetzt jede Meldung hinzugefügt und alle können angezeigt werden.
            List<Message> messages = (List<Message>) CurrentController.TempData[TempMessageString];
            //Wenn noch keine Nachrichten hinterlegt wurden, muss die Liste initialisiert werden
            if (messages == null)
            {
                messages = new List<Message>();
            }

            messages.Add(new Message(message, messageType));

            //Die Message im TempData hinterlegen, damit diese dann von der Passenden Helper Methode dargestellt werden kann.
            CurrentController.TempData[TempMessageString] = messages;
        }
    }
    #endregion
}

Das Hinzufügen von CustomMessages ist überall da möglich wo Ihr Zugriff auf euren Controller habt, ich übergebe meist auch an mein Model den aktuellen Controller, um gleich in der Logik die passenden Messages hinzufügen zu können.

 CustomMessage.CurrentMessage(this).AddMessage("Warnung 1", MessageTypes.warning);
 CustomMessage.CurrentMessage(this).AddMessage("Warnung 2", MessageTypes.warning);
 CustomMessage.CurrentMessage(this).AddMessage("Bitte passen Sie auf!", MessageTypes.message);
 CustomMessage.CurrentMessage(this).AddMessage("Speichervorgang erfolgreich", MessageTypes.ok);

Im View selbst wird nur der HTML Helper “ShowCustomMessage” aufgerufen und dieser rendert jeden Fehlermeldung in einer Eigenen Box.

<div class="row">
    <div class="span12">
        @Html.ShowCustomMessage()
    </div>
</div>

Damit das ganze auch “gut” aussieht, ist noch die Einbindung von Twitter Bootstrap notwendig. Ihr könnt aber auch selbst die HTML Templates an eure Bedürfnisse anpassen.