Guten Tag.
Ich habe WPF viele Jahre lang benutzt. Das MVVM-Muster ist wahrscheinlich eines der bequemsten Architekturmuster. Ich nahm an, dass MVC fast gleich ist. Als ich den Einsatz von MVC in der Praxis an einem neuen Arbeitsplatz sah, war ich überrascht über die Komplexität und gleichzeitig den Mangel an elementarer Benutzerfreundlichkeit. Am ärgerlichsten ist, dass die Validierung nur erfolgt, wenn das Formular überladen ist. Es gibt keine roten Rahmen, die das Feld hervorheben, in dem der Fehler aufgetreten ist, sondern nur eine Warnung mit einer Liste von Fehlern. Wenn viele Fehler vorliegen, müssen Sie einige der Fehler korrigieren und speichern, um sie zu speichern, um die Validierung zu wiederholen. Die Schaltfläche Speichern ist immer aktiv. Verknüpfte Listen werden wirklich über js implementiert, aber es ist kompliziert und verwirrend. Das Modell, die Ansicht und der Controller sind eng miteinander verbunden. Testen Sie also alles Pracht sehr schwierig.
Wie gehe ich damit um? An wen es interessant ist frage ich unter kat.
Und so haben wir:
Die Erstellung von MVC-Formularen in klassischer Form impliziert keine andere Art der Interaktion mit dem Server, da die gesamte Seite überlastet wird, was für den Benutzer nicht bequem ist.
Die vollständige Nutzung von Frameworks wie Reart, Angular, Vue und der Übergang zu SinglePageApplicatrion würden bequemere Schnittstellen darstellen. Leider ist dies im Rahmen dieses Projekts im Prinzip nicht möglich, da:
- Viele Codes wurden geschrieben und akzeptiert, und niemand lässt Sie sie wiederholen.
-Wir sind C # -Programmierer und kennen js nicht in der richtigen Menge.
Außerdem werden die Reart-, Angular- und Vue-Frameworks geschärft, um komplexe Logik auf dem Client zu schreiben, was in meinem WPF-Look nicht korrekt ist. Die gesamte Logik sollte sich an einem Ort befinden, und dies ist ein Geschäftsobjekt und / oder eine Modellklasse. Die Ansicht sollte nur den Status des Modells nicht mehr anzeigen.
Auf der Grundlage des Vorstehenden habe ich versucht, einen Ansatz zu finden, mit dem Sie die maximale Funktionalität mit einem Minimum an js-Code erzielen können. Zunächst der Mindestcode, der geschrieben werden muss, um ein bestimmtes Feld auszugeben und zu aktualisieren.
Mein vorgeschlagenes VueJs + MVC- Bundle sieht folgendermaßen aus:
- VueJs wird in der einfachsten Version mit Verbindung über CDN verwendet. Komponenten bei Bedarf können über CDN angeschlossen werden.
- Nach dem Laden lädt Vue die Formulardaten über Ajax.
- Jedes Mal, wenn sich das Formular ändert, sendet Vue alle Änderungen an den Server (für Textfelder können Sie konfigurieren, dass die Änderungen gesendet werden, wenn der Fokus verloren geht).
- Die Validierung erfolgt auf dem Server über den Entitätsmechanismus, und ungültige Felder werden an den Client zurückgegeben und ein Zeichen dafür, dass sich der Status des Modells in Bezug auf die Datenbank geändert hat.
-Wenn die nächste Validierungsanforderung früher als die vorherige erfolgt, wird die vorherige Validierungsanforderung abgebrochen.
MVC-Modell wird nicht verwendet. Die ViewModel-Funktion im WPF-Sinne ist hier zwischen vue und Controller unscharf.
Die Vorteile einer solchen Implementierung gegenüber der klassischen Razor-Seite: - Die Benutzeroberfläche wird mit Vue-Werkzeugen gezeichnet, die zum Zeichnen von Benutzeroberflächen vorgesehen sind. Der Hauptvorteil.
- Trennen von Ansichtsebenen von ViewModel.
- Validierungsfehler werden beim Ausfüllen des Formulars angezeigt.
- Convenience-Tests
Nachteile: - Übermäßige Belastung des Servers mit Validierungsanforderungen.
Die Notwendigkeit, vue und js auf ein Minimum zu kennen.
Ich betrachte diesen Ansatz als erste Vorlage für die Arbeit mit dem Formular.
In einer realen Anwendung für eine bestimmte Form ist es wünschenswert, Folgendes zu optimieren:
1) Senden Sie eine Validierungsanforderung nur, wenn Sie Felder ändern, die auf dem Server validiert werden müssen.
2) Die Validierung ist lang, die Felder sind voll usw. auf dem Client ausführen.
Also lass uns gehen.
In meinem Beispiel habe ich als Datenbank die Trainingsdatenbank Northwind verwendet, die ich mit einem der Devextreem-Beispiele heruntergeladen habe.
Anwendungserstellung, Verbindung von Entity und Erstellung von DbContext werde ich hinter den Kulissen lassen. Link zu Github mit einem Beispiel am Ende des Artikels.
Erstellen Sie einen neuen leeren MVC 5-Controller und nennen Sie ihn OrdersController. Bisher gibt es eine Methode.
public ActionResult Index() { return View(); }
Fügen Sie noch eine hinzu
public ActionResult Edit() { return View(); }
Jetzt müssen Sie zum Ordner Ansichten / Bestellungen gehen und zwei Seiten Index.cshtml und Edit.cshtml hinzufügen
Ein wichtiger Hinweis, dass eine cshtml-Seite ohne Modell funktionieren würde, muss oben auf der geerbten System.Web.Mvc.WebViewPage-Seite hinzugefügt werden.
Es wird davon ausgegangen, dass Index.cshtml eine Tabelle enthält, aus der eine hervorgehobene Zeile zur Bearbeitungsseite führt. Erstellen Sie zunächst nur Links, die zur Bearbeitungsseite führen.
@inherits System.Web.Mvc.WebViewPage <table > @foreach (var item in ViewBag.Orders) { <tr><td><a href="Edit?id=@item.OrderID">@item.OrderID</a></td></tr> } </table>
Jetzt möchte ich die Bearbeitung eines vorhandenen Objekts implementieren.
Als erstes muss eine Methode in der Steuerung beschrieben werden, die eine Objektbeschreibung per Kennung an den Json-Client zurückgibt.
[HttpGet] public ActionResult GetById(int id) { var order = _db.Orders.Find(id);
Sie können überprüfen, ob alles funktioniert, indem Sie im Browser (die Portnummer gehört natürlich Ihnen) http: // localhost: 63164 / Orders / GetById? Id = 10501 eingeben
Sie sollten so etwas im Browser bekommen
{ "OrderID": 10501, "CustomerID": "BLAUS", "EmployeeID": 9, "OrderDate": "1997-04-09T00:00:00", "RequiredDate": "1997-05-07T00:00:00", "ShippedDate": "1997-04-16T00:00:00", "ShipVia": 3, "Freight": 8.85, "ShipName": "Blauer See Delikatessen", "ShipAddress": "Forsterstr. 57", "ShipCity": "Mannheim", "ShipRegion": null, "ShipPostalCode": "68306", "ShipCountry": "Germany" }
Nun und (oder) einen einfachen Test schreiben. Lassen wir das Testen jedoch über den Rahmen dieses Artikels hinausgehen.
[Test] public void OrderControllerGetByIdTest() { var bdContext = new Northwind(); var id = bdContext.Orders.First().OrderID;
Als Nächstes müssen Sie ein Vue-Formular erstellen.
@inherits System.Web.Mvc.WebViewPage <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> </title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <h1>A </h1> <table > <tr v-for="(item,i) in order"> @* *@ <td> {{i}}</td> <td> <input type="text" v-model="order[i]"/> </td> </tr> </table> </div> <script> new Vue({ el: "#app", data: { order: { OrderID: 10501, CustomerID: "BLAUS", EmployeeID: 9, OrderDate: "1997-04-09T00:00:00", RequiredDate: "1997-05-07T00:00:00", ShippedDate: "1997-04-16T00:00:00", ShipVia: 3, Freight: 8.85, ShipName: "Blauer See Delikatessen", ShipAddress: "Forsterstr. 57", ShipCity: "Mannheim", ShipRegion: null, ShipPostalCode: "68306", ShipCountry: "Germany" } } }); </script> </body> </html>
Wenn alles richtig gemacht wurde, sollte der Prototyp des zukünftigen Formulars im Browser angezeigt werden.

Wie wir sehen können, zeigte Vue alle Felder genau so an, wie das Modell war. Die Daten im Modell sind jedoch immer noch statisch. Als Erstes müssen Sie das Laden von Daten aus der Datenbank mithilfe der gerade geschriebenen Methode implementieren.
Fügen Sie dazu die Methode fetchOrder () hinzu und rufen Sie sie im gemounteten Abschnitt auf:
new Vue({ el: "#app", data: { id: @ViewBag.Id, order: { OrderID: 0, CustomerID: "", EmployeeID: 0, OrderDate: "", RequiredDate: "", ShippedDate: "", ShipVia: 0, Freight: 0, ShipName: "0", ShipAddress: "", ShipCity: "", ShipRegion: null, ShipPostalCode: "", ShipCountry: "" }, }, methods: {
Nun, da der Bezeichner des Objekts jetzt vom Controller stammen sollte, müssen Sie im Controller den Bezeichner an das dynamische ViewBag-Objekt übergeben, damit er in View abgerufen werden kann.
public ActionResult SimpleEdit(int id = 0) { ViewBag.Id = id; return View(); }
Dies reicht aus, um die Daten beim Booten zu lesen.
Es ist Zeit, das Formular anzupassen.
Um den Artikel nicht zu überladen, habe ich ein Minimum an Feldern abgeleitet. Ich empfehle für den Anfang, herauszufinden, wie man mit verknüpften Listen arbeitet.
<table > <tr> <td> </td> <td > <input type="number" v-model="order.Freight" /> </td> </tr> <tr> <td> </td> <td> <input type="text" v-model="order.ShipCountry" /> </td> </tr> <tr> <td> </td> <td> <input type="text" v-model="order.ShipCity" /> </td> </tr> <tr> <td> </td> <td> <input type="text" v-model="order.ShipAddress" /> </td> </tr> </table>
Die Felder ShipCountry und ShipAddress sind die besten Kandidaten für verknüpfte Listen.
Hier sind die Controller-Methoden. Wie Sie sehen können, ist alles ziemlich einfach. Die gesamte Filterung erfolgt mit Linq.
Aber in der Ansicht wurde Code deutlich mehr hinzugefügt.
Zusätzlich zu den Funktionen der Länder und Städte müssen Sie eine Uhr hinzufügen, die die Änderungen des Objekts überwacht. Leider wird der alte Wert des komplexen Vue-Objekts nicht gespeichert. Sie müssen ihn daher manuell speichern, für die ich die Methode saveOldOrderValue entwickelt habe: Während ich nur das Land darin speichere. Auf diese Weise können Sie die Liste der Städte nur dann erneut lesen, wenn sich das Land ändert. Ansonsten ist der Code der gleiche, denke ich. Im Beispiel habe ich nur eine verknüpfte Liste mit einer Ebene angezeigt (nach diesem Prinzip ist es nicht schwierig, eine Ebene zu verschachteln).
@inherits System.Web.Mvc.WebViewPage <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> </title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <table> <tr> <td>C </td> <td> <input type="number" v-model="order.Freight" /> </td> </tr> <tr> <td> </td> <td> <select v-model="order.ShipCountry" class="input"> <option v-for="item in AvaialbeCountrys" :v-key="item">{{item}} </option> </select> </td> </tr> <tr> <td> </td> <td> <select v-model="order.ShipCity" > <option v-for="city in AvaialbeCitys" :v-key="city">{{city}} </option> </select> </td> </tr> <tr> <td> </td> <td> <input type="text" v-model="order.ShipAddress" /> </td> </tr> </table> </div> <script> new Vue({ el: "#app", data: { id: @ViewBag.Id, order: { OrderID: 0, CustomerID: "", EmployeeID: 0, OrderDate: "", RequiredDate: "", ShippedDate: "", ShipVia: 0, Freight: 0, ShipName: "0", ShipAddress: "", ShipCity: "", ShipRegion: null, ShipPostalCode: "", ShipCountry: "" }, oldOrder: { ShipCountry: "" }, AvaialbeCitys: [], AvaialbeCountrys: [] }, methods: { </script> </body> </html>
Ein separates Thema ist die Validierung. Unter dem Gesichtspunkt der Geschwindigkeitsoptimierung müssen Sie natürlich eine Validierung auf dem Client durchführen. Dies führt jedoch zu einer Duplizierung des Codes. Daher zeige ich ein Beispiel mit Validierung auf Entitätsebene (wie es idealerweise sein sollte). Gleichzeitig, dem Mindestcode, erfolgt die Validierung selbst recht schnell und auch asynchron. Wie die Praxis gezeigt hat, funktioniert auch bei einem sehr langsamen Internet alles mehr als normal.
Probleme treten nur auf, wenn Text schnell in ein Textfeld eingegeben wird und die Schreibgeschwindigkeit 260 Zeichen pro Minute beträgt. Die einfachste Optimierungsoption für Textfelder besteht darin, das verzögerte Update-V-Modell .lazy = "order.ShipAddress" festzulegen. Die Validierung erfolgt dann, wenn sich der Fokus ändert. Eine erweiterte Option besteht darin, die Validierungsverzögerung für diese Felder + zu verzögern, wenn die nächste Validierungsanforderung vor dem Empfang einer Antwort aufgerufen wird, und dann die Verarbeitung der vorherigen Anforderung zu ignorieren.
Die Methoden zur Verarbeitung der Validierung in der Kontrolle waren die folgenden.
[HttpGet] public ActionResult Validate(int id, string json) { var order = _db.Orders.Find(id); JsonConvert.PopulateObject(json, order); var errorsD = GetErrorsJsArrey(); return Content(errorsD.ToString(), "application/json"); } private String GetErrorsAndChanged() { var changed= _db.ChangeTracker.HasChanges(); var errors = _db.GetValidationErrors(); return GetErrorsAndChanged(errors,changed); } private static string GetErrorsAndChanged(IEnumerable<DbEntityValidationResult> errors,bool changed) { dynamic dynamic = new ExpandoObject(); dynamic.IsChanged = changed;
DynObject
public sealed class DynObject : DynamicObject { private readonly Dictionary<string, object> _properties; public DynObject(Dictionary<string, object> properties) { _properties = properties; } public override IEnumerable<string> GetDynamicMemberNames() { return _properties.Keys; } public override bool TryGetMember(GetMemberBinder binder, out object result) { if (_properties.ContainsKey(binder.Name)) { result = _properties[binder.Name]; return true; } else { result = null; return false; } } public override bool TrySetMember(SetMemberBinder binder, object value) { if (_properties.ContainsKey(binder.Name)) { _properties[binder.Name] = value; return true; } else { return false; } } }
Ziemlich ausführlich, aber dieser Code wird einmal für die gesamte Anwendung geschrieben und erfordert keine Optimierung für ein bestimmtes Objekt oder Feld. Als Ergebnis der auf dem Client arbeitenden Methode das json-Objekt mit den Eigenschaften IsChanded und Errors. Diese Eigenschaften müssen natürlich in unserem Vue erstellt und bei jeder Änderung des Objekts ausgefüllt werden.
Um Validierungsfehler zu erhalten, müssen Sie diese Validierung irgendwo festlegen. Es ist an der Zeit, unserer Beschreibung der Entität des Auftragsobjekts einige Validierungsattribute hinzuzufügen.
[MinLength(10)] [StringLength(60)] public string ShipAddress { get; set; } [CheckCityAttribute(" ShipCity ")] public string ShipCity { get; set; }
MinLength und StringLength sind Standardattribute, aber für ShipCity habe ich ein benutzerdefiniertes Attribut erstellt
Lassen wir das Thema der Entitätsvalidierung jedoch auch außerhalb des Geltungsbereichs dieses Artikels.
Um Fehler anzuzeigen, müssen Sie einen Link zu CSS hinzufügen und das Formular leicht ändern.
So sollte unser modifiziertes Formular nun aussehen:
@inherits System.Web.Mvc.WebViewPage <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> id=@ViewBag.Id</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <link rel="stylesheet" type="text/css" href="~/Content/vueError.css" /> </head> <body> <div id="app"> <table> <tr> <td> </td> <td class="tooltip"> <input type="number" v-model="order.Freight" v-bind:class="{error:!errors.Freight==''} " class="input" /> <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span> </td> </tr> <tr> <td> </td> <td> <select v-model="order.ShipCountry" class="input"> <option v-for="item in AvaialbeCountrys" :v-key="item">{{item}} </option> </select> </td> </tr> <tr> <td> </td> <td class="tooltip"> <select v-model="order.ShipCity" v-bind:class="{error:!errors.ShipCity==''}" class="input"> <option v-for="city in AvaialbeCitys" :v-key="city">{{city}} </option> </select> <span v-if="!errors.ShipCity==''" class="tooltiptext">{{errors.ShipCity}}</span> </td> </tr> <tr> <td> </td> <td class="tooltip"> <input type="text" v-model.lazy="order.ShipAddress" v-bind:class="{error:!errors.ShipAddress=='' }" class="input" /> <span v-if="!errors.ShipAddress==''" class="tooltiptext">{{errors.ShipAddress}}</span> </td> </tr> <tr> <td> </td> <td> <button v-on:click="Save()" :disabled="IsChanged===false" || hasError class="alignRight">Save</button> </td> </tr> </table> </div> <script> new Vue({ el: "#app", data: { id: @ViewBag.Id, order: { OrderID: 0, CustomerID: "", EmployeeID: 0, OrderDate: "", RequiredDate: "", ShippedDate: "", ShipVia: 0, Freight: 0, ShipName: "0", ShipAddress: "", ShipCity: "", ShipRegion: null, ShipPostalCode: "", ShipCountry: "" }, oldOrder: { ShipCountry: "" }, errors: { OrderID: null, CustomerID: null, EmployeeID: null, OrderDate: null, RequiredDate: null, ShippedDate: null, ShipVia: null, Freight: null, ShipName: null, ShipAddress: null, ShipCity: null, ShipRegion: null, ShipPostalCode: null, ShipCountry: null }, IsChanged: false, AvaialbeCitys: [], AvaialbeCountrys: [] }, computed : { hasError: function () { for (var err in this.errors) { var error = this.errors[err]; if (error !== '' || null) return true; } return false; } }, methods: { </script> </body> </html>
Es sieht aus wie CSS
.tooltip { position: relative; display: inline-block; border-bottom: 1px dotted black; } .tooltip .tooltiptext { visibility: hidden; width: 120px; background-color: #555; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; bottom: 125%; left: 50%; margin-left: -60px; opacity: 0; transition: opacity 0.3s; } .tooltip .tooltiptext::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #555 transparent transparent transparent; } .tooltip:hover .tooltiptext { visibility: visible; opacity: 1; } .error { color: red; border-color: red; border-style: double; } .input { width: 200px ; } .alignRight { float: right }
Und hier ist das Ergebnis der Arbeit.

Um zu verstehen, wie die Validierung funktioniert, schauen wir uns das Markup an, das ein Feld beschreibt:
<td class="tooltip"> <input type="number" **v-model="order.Freight" v-bind:class="{error:!errors.Freight==''} " **class="input" /> <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span> </td>
Hier sind 2 wichtige Schlüsselpunkte:
Dieser Teil des Markups verbindet den Stil, der für den roten Rahmen um das v-bind- Element verantwortlich ist : class = "{error:! Errors.Freight == ''} hier verbindet vue die CSS-Klasse nach Bedingung.
Und hier ist dieses Popup-Fenster, das angezeigt wird, wenn sich der Mauszeiger über einem Element befindet:
<span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span>
Darüber hinaus muss das übergeordnete Element das Attribut class = "tooltip" enthalten.
In der letzten Version wurde die Schaltfläche Speichern so konfiguriert hinzugefügt, dass sie nur verfügbar ist, wenn das Speichern möglich ist.
Um das für die Validierung erforderliche Markup zu vereinfachen, schlage ich vor, die einfachste Komponente zu schreiben, die die gesamte Validierung auf sich nimmt.
Vue.component('error-aborder', { props: { error: String }, template: '<div class="tooltip" >' + '<div v-bind:class="{error:!error==\'\' }" >' + '<slot>test</slot>' + '</div>' + '<p class="tooltiptext" v-if="!error==\'\'" >{{error}}</p>' + '</div>' });
Jetzt sieht das Markup ordentlicher aus.
<error-aborder v-bind:error="errors.Freight"> <input type="number" v-model="order.Freight" class="input" /> </error-aborder>
Bei der Entwicklung geht es darum, Felder in einem Formular anzuordnen, die Validierung in Entyty einzurichten und Listen zu erstellen. Wenn die Listen statisch und nicht groß sind, können sie vollständig im Code festgelegt werden.
Der C # -Teil des Codes ist gut getestet. Die nächsten Pläne befassen sich mit Vue-Tests.
Das ist alles was ich erzählen wollte.
Ich würde mich über konstruktive Kritik sehr freuen.
Hier ist der Link zum Quellcode .
Im Beispiel heißt das Formular SimpleEdit und enthält die neueste Version. Jeder, der an vorläufigen Optionen interessiert ist, kann die Commits durchgehen.
Im Beispiel habe ich die Optimierung implementiert: Abbrechen der Validierungsanforderung, wenn, ohne auf die Validierungsantwort zu warten, die Validierung ein zweites Mal verursacht wird.