Código mínimo VueJs + MVC funcionalidad máxima

Buenas tardes


Usé WPF por muchos años. El patrón MVVM es probablemente uno de los patrones arquitectónicos más convenientes. Asumí que MVC es casi lo mismo. Cuando vi el uso de MVC en la práctica en un nuevo lugar de trabajo, me sorprendió la complejidad y, al mismo tiempo, la falta de usabilidad elemental. Lo más molesto es que la validación solo ocurre cuando el formulario está sobrecargado. No hay marcos rojos que resalten el campo en el que se produjo el error, sino solo una alerta con una lista de errores. Si hay muchos errores, debe corregir algunos de los errores y guardar para guardar para repetir la validación. El botón Guardar siempre está activo. Las listas vinculadas se implementan realmente a través de js, pero es complicado y confuso. El modelo, la vista y el controlador están estrechamente acoplados, así que pruébelo todo magnificencia Muy dificil.
¿Cómo lidiar con esto? Para quién es interesante, pregunto bajo kat.


Y entonces tenemos:
La construcción de formularios MVC en una forma clásica no implica otra forma de interactuar con el servidor, ya que sobrecarga la página completa, lo que no es conveniente para el usuario.
El uso completo de marcos como Reart, Angular, Vue y la transición a SinglePageApplicatrion permitiría crear interfaces más convenientes, pero desafortunadamente, en principio, no es posible dentro del marco de este proyecto ya que:
-Mucho código ha sido escrito, aceptado, y nadie te permitirá rehacerlo.
-Somos programadores de C # y no sabemos js en la cantidad correcta.


Además, los marcos Reart, Angular, Vue se agudizan para escribir una lógica compleja en el cliente, lo que no es correcto en mi vista WPF. Toda la lógica debe estar en un solo lugar y este es un objeto de negocio y / o clase de modelo. Ver solo debe mostrar el estado del modelo no más.
Basado en lo anterior, traté de encontrar un enfoque que le permita obtener la máxima funcionalidad con un mínimo de código js. En primer lugar, el código mínimo que debe escribirse en la salida y actualizar un campo específico.
Mi paquete VueJs + MVC propuesto se ve así:


  • VueJs se utiliza en la versión más simple con conexión a través de cdn. Los componentes, si se requieren, pueden conectarse a través de cdn.
  • Después de cargar, Vue carga los datos del formulario a través de Ajax.
  • Cada vez que cambia el formulario, Vue envía todos los cambios al servidor (para los campos de texto, puede configurar que los cambios se envíen cuando se pierde el foco).
  • La validación se produce en el servidor a través del mecanismo de entidad y los campos no válidos se devuelven al cliente y una señal de que el estado del modelo ha cambiado con respecto a la base de datos.
    -Si la próxima solicitud de validación se produce antes que la anterior, la solicitud de validación anterior se cancelará.
    El modelo MVC no se usa. La función ViewModel en el sentido WPF está borrosa aquí entre vue y el controlador.
    Las ventajas de tal implementación sobre la página clásica de Razor:
  • La interfaz se dibuja utilizando las herramientas Vue, que está diseñada para dibujar interfaces. La principal ventaja.
  • Separar las capas de vista del modelo de vista.
  • los errores de validación se muestran a medida que se llena el formulario.
  • prueba de conveniencia
    Desventajas
  • Carga excesiva en el servidor con solicitudes de validación.
  • La necesidad de saber vue y js al mínimo.


    Considero este enfoque como la plantilla inicial para trabajar con el formulario.
    En una aplicación real para un formulario específico, es deseable optimizar:
    1) Envíe una solicitud de validación solo cuando cambie los campos, que deben validarse en el servidor.
    2) La validación es larga, los campos están llenos, etc. ejecutar en el cliente



Entonces vamos.


En mi ejemplo, utilicé la base de datos de entrenamiento Northwind, que descargué con uno de los ejemplos de Devextreem.
Creación de aplicaciones, conexión de Entity y creación de DbContext Dejaré detrás de escena. Enlace a github con un ejemplo al final del artículo.
Cree un nuevo controlador MVC 5 vacío. Llámelo OrdersController. Tiene un método hasta ahora.


public ActionResult Index() { return View(); } 

Agrega uno más


  public ActionResult Edit() { return View(); } 

Ahora debe ir a la carpeta Vistas / Pedidos y agregar dos páginas Index.cshtml y Edit.cshtml
Una nota importante de que una página cshtml funciona sin un modelo debe agregarse en la parte superior de la página heredada System.Web.Mvc.WebViewPage.
Se supone que Index.cshtml contiene una tabla desde la cual una línea resaltada irá a la página de edición. Por ahora, solo cree enlaces que conduzcan a la página de edición.


 @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> 

Ahora quiero implementar la edición de un objeto existente.


Lo primero que debe hacer es describir un método en el controlador que devolvería una descripción de objeto al cliente Json por identificador.


  [HttpGet] public ActionResult GetById(int id) { var order = _db.Orders.Find(id);//  string orderStr = JsonConvert.SerializeObject(order);//  return Content(orderStr, "application/json");// } 

Puede verificar que todo funciona escribiendo en el navegador (el número de puerto es naturalmente suyo) http: // localhost: 63164 / Orders / GetById? Id = 10501
Deberías obtener algo como esto en el navegador


 { "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" } 

Bueno y (o) escribiendo una prueba simple. Sin embargo, dejemos las pruebas más allá del alcance de este artículo.


  [Test] public void OrderControllerGetByIdTest() { var bdContext = new Northwind(); var id = bdContext.Orders.First().OrderID; //    var orderController = new OrdersController(); var json = orderController.GetById(id) as ContentResult; var res = JsonConvert.DeserializeObject(json.Content,typeof(Order)) as Order; Assert.AreEqual(id, res.OrderID); } 

A continuación, debe crear un formulario Vue.


 @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> 

Si todo se hace correctamente, el prototipo del formulario futuro debe mostrarse en el navegador.



Como podemos ver, Vue mostró todos los campos exactamente como era el modelo. Pero los datos en el modelo siguen siendo estáticos y lo primero que debe hacer a continuación es implementar la carga de datos desde la base de datos a través del método recién escrito.
Para hacer esto, agregue el método fetchOrder () y llámelo en la sección montada:


  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: { //  fetchOrder() { var path = "../Orders/GetById?key=" + this.id; console.log(path); this.fetchJson(path, json => this.order = json); }, //    fetch fetchJson(path, collback) { try { fetch(path, { mode: 'cors' }) .then(response => response.json()) .then(function(json) { collback(json); } ); } catch (ex) { alert(ex); } } }, mounted: function() { this.fetchOrder(); } }); 

Bueno, dado que el identificador del objeto ahora debe provenir del controlador, entonces en el controlador debe pasar el identificador al objeto ViewBag dinámico, para que pueda obtenerse en la Vista.


  public ActionResult SimpleEdit(int id = 0) { ViewBag.Id = id; return View(); } 

Esto es suficiente para leer los datos en el momento del arranque.
Es hora de personalizar el formulario.
Para no sobrecargar el artículo, deduje un mínimo de campos. Sugiero a los principiantes que descubran cómo trabajar con listas vinculadas.


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

Los campos ShipCountry y ShipAddress son los mejores candidatos para las listas vinculadas.
Aquí están los métodos del controlador. Como puede ver, todo es bastante simple: todo el filtrado se realiza utilizando Linq.


  /// <summary> ///    c     ///       ,    /// </summary> /// <param name="country"></param> /// <param name="region"></param> /// <returns></returns> [HttpGet] public ActionResult AvaiableCityList( string country,string region=null) { var avaiableCity = _db.Orders.Where(c => ((c.ShipRegion == region) || region == null)&& (c.ShipCountry == country) || country == null).Select(a => a.ShipCity).Distinct(); var jsonStr = JsonConvert.SerializeObject(avaiableCity); return Content(jsonStr, "application/json"); } /// <summary> ///    c   ///    ,    /// </summary> /// <param name="region"></param> /// <returns></returns> [HttpGet] public ActionResult AvaiableCountrys(string region=null) { var resList = _db.Orders.Where(c => (c.ShipRegion == region)||region==null).Select(c => c.ShipCountry).Distinct(); var json = JsonConvert.SerializeObject(resList); return Content(json, "application/json"); } 

Pero en el código de vista se ha agregado significativamente más.
Además de las funciones de los países y ciudades, debe agregar un reloj que supervise los cambios del objeto, desafortunadamente el valor antiguo del objeto vue complejo no se guarda, por lo que debe guardarlo manualmente, para lo cual se me ocurrió el método saveOldOrderValue: mientras guardo solo el país en él. Esto le permite volver a leer la lista de ciudades solo cuando cambia el país. De lo contrario, el código es el mismo, creo. En el ejemplo, mostré solo una lista vinculada de un solo nivel (por este principio no es difícil anidar en ningún nivel).


 @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: { //  fetchOrder() { var path = "../Orders/GetById?Id=" + this.id; this.fetchJson(path, json => this.order = json); }, fetchCityList() { //     var country = this.order.ShipCountry; if (country == null || country === "") { country = ''; } var path = "../Orders/AvaiableCityList?country=" + country; this.fetchJson(path, json => {this.AvaialbeCitys = json;}); }, fetchCountrys() { var path = "../Orders/AvaiableCountrys"; this.fetchJson(path,jsonResult => {this.AvaialbeCountrys = jsonResult;}); }, //    fetch fetchJson(path, collback) { try { fetch(path, { mode: 'cors' }) .then(response => response.json()) .then(function(json) { collback(json); } ); } catch (ex) { alert(ex); } }, saveOldOrderValue:function(){ this.oldOrder.ShipCountry = this.order.ShipCountry; } }, watch: { order: { handler: function (after) { if (this.oldOrder.ShipCountry !== after.ShipCountry)//    { this.fetchCityList();//       } this.saveOldOrderValue(); }, deep: true } }, mounted: function () { this.fetchCountrys();//   //    ,      this.fetchOrder();//  this.saveOldOrderValue();//   } }); </script> </body> </html> 

Un tema aparte es la Validación. Desde el punto de vista de la optimización de la velocidad, por supuesto, debe realizar la validación en el cliente. Pero esto conducirá a la duplicación de código, por lo que estoy mostrando un ejemplo con validación a nivel de entidad (como debería ser idealmente). Al mismo tiempo, el código mínimo, la validación en sí misma ocurre bastante rápido y también de forma asincrónica. Como la práctica ha demostrado, incluso con un Internet muy lento, todo funciona más de lo normal.
Los problemas solo surgen si el texto se escribe rápidamente en un campo de texto y la velocidad de escritura es de 260 caracteres por minuto. La opción de optimización más simple para los campos de texto es establecer la actualización diferida v-model .lazy = "order.ShipAddress", luego la validación ocurrirá cuando cambie el foco. Una opción más avanzada es hacer un retraso de validación para estos campos + si se llama a la siguiente solicitud de validación antes de recibir una respuesta, luego ignorar el procesamiento de la solicitud anterior.
Los métodos para procesar la validación en el control fueron los siguientes.


  [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;//  IsChanged var errProperty = new Dictionary<string, object>();//      dynamic.Errors = new DynObject(errProperty);//        foreach (DbEntityValidationResult validationError in errors)//   { foreach (DbValidationError err in validationError.ValidationErrors)//   { errProperty.Add(err.PropertyName,err.ErrorMessage); } } var json = JsonConvert.SerializeObject(dynamic); return json; } 

     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; } } } 

Muy detallado, pero este código se escribe una vez para toda la aplicación y no requiere ajuste para un objeto o campo específico. Como resultado del método de trabajo en el cliente, el objeto json con las propiedades IsChanded y Errores. Estas propiedades naturalmente deben crearse en nuestro Vue y completarse con cada cambio del objeto.
Para obtener errores de validación, debe establecer esta validación en algún lugar. Es hora de agregar algunos atributos de validación a nuestra descripción del objeto Entidad del pedido.


  [MinLength(10)] [StringLength(60)] public string ShipAddress { get; set; } [CheckCityAttribute(" ShipCity   ")] public string ShipCity { get; set; } 

MinLength y StringLength son atributos estándar, pero para ShipCity creé un atributo personalizado


  /// <summary> /// Custom Attribute Example /// </summary> [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class CheckCityAttribute : ValidationAttribute { public CheckCityAttribute(string message) { this.ErrorMessage = message; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { ValidationResult result = ValidationResult.Success; string[] memberNames = new string[] { validationContext.MemberName }; string val = value?.ToString(); Northwind _db = new Northwind(); Order order = (Order)validationContext.ObjectInstance; bool exsist = _db.Orders.FirstOrDefault(o => o.ShipCity == val && o.ShipCountry == order.ShipCountry)!=null; if (!exsist) { result = new ValidationResult(string.Format(this.ErrorMessage,order.ShipCity , val), memberNames); } return result; } } 

Sin embargo, dejemos el tema de la validación de entidades también fuera del alcance de este artículo.
Para mostrar errores, debe agregar un enlace a Css y modificar ligeramente el formulario.
Así es como debería verse nuestro formulario modificado:


 @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: { //  fetchOrder() { var path = "../Orders/GetById?Id=" + this.id; this.fetchJson(path, json => this.order = json); }, fetchCityList() { //     var country = this.order.ShipCountry; if (country == null || country === "") { country = ''; } var path = "../Orders/AvaiableCityList?country=" + country; this.fetchJson(path, json => {this.AvaialbeCitys = json;}); }, fetchCountrys() { var path = "../Orders/AvaiableCountrys"; this.fetchJson(path,jsonResult => {this.AvaialbeCountrys = jsonResult;}); }, //    fetch Validate() {this.Action("Validate");}, Save() {this.Action("Save");}, Action(action) { var myJSON = JSON.stringify(this.order); var path = "../Orders/" + action + "?id=" + this.id + "&json=" + myJSON; this.fetchJson(path, jsonResult => { this.errors = jsonResult.Errors; this.IsChanged = jsonResult.IsChanged; }); }, fetchJson(path, collback) { try { fetch(path, { mode: 'cors' }) .then(response => response.json()) .then(function(json) { collback(json); } ); } catch (ex) { alert(ex); } }, saveOldOrderValue:function(){ this.oldOrder.ShipCountry = this.order.ShipCountry; } }, watch: { order: { handler: function (after) { this.IsChanged=true; if (this.oldOrder.ShipCountry !== after.ShipCountry)//    { this.fetchCityList();//       } this.saveOldOrderValue(); this.Validate(); }, deep: true } }, mounted: function () { this.fetchCountrys();//   //    ,      this.fetchOrder();//  this.saveOldOrderValue();//   } }); </script> </body> </html> 

Parece 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 } 

Y aquí está el resultado del trabajo.



Para comprender cómo funciona la validación, veamos cuidadosamente el marcado que describe un campo:


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

Aquí hay 2 puntos clave importantes:


Esta parte del marcado conecta el estilo responsable del borde rojo alrededor del elemento v-bind: class = "{error :! errors.freight == ''} aquí vue conecta la clase css por condición.


Y aquí está esta ventana emergente que se muestra cuando el cursor del mouse está sobre un elemento:


  <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span> 

Además, el elemento padre debe contener el atributo class = "tooltip".


En la última versión, el botón Guardar se agrega configurado para que esté disponible solo si es posible guardar.
Para simplificar el marcado necesario para la validación, propongo escribir el componente más simple que tomaría toda la validación sobre sí mismo.


 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>' }); 

ahora el marcado se ve más ordenado.


  <error-aborder v-bind:error="errors.Freight"> <input type="number" v-model="order.Freight" class="input" /> </error-aborder> 

El desarrollo se reduce a organizar los campos en un formulario, configurar la validación en Entyty y crear listas. Si las listas son estáticas y no son grandes, se pueden configurar completamente en el código.


La parte C # del código está bien probada. Los próximos planes tratan con las pruebas de Vue.


Eso es todo lo que quería contar.
Agradecería enormemente la crítica constructiva.


Aquí está el enlace al código fuente .


En el ejemplo, el formulario se llama SimpleEdit y contiene la última versión. Cualquier persona interesada en opciones preliminares puede pasar por los commits.
En el ejemplo, implementé la optimización: abortar la solicitud de validación si, sin esperar la respuesta de validación, causa la validación por segunda vez.

Source: https://habr.com/ru/post/455367/


All Articles